Merge pull request #1919 from qwibitai/v2
v2: ground-up architectural rewrite
This commit is contained in:
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
||||
{"sessionId":"bedd47ed-bfa0-41da-9a03-93d41159b4cd","pid":24606,"acquiredAt":1776194767342}
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"sandbox": {
|
||||
"network": {
|
||||
"allowedDomains": [
|
||||
"npm.registry.com",
|
||||
"us.i.posthog.com"
|
||||
]
|
||||
}
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
**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
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
### 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.
|
||||
138
.claude/skills/add-dashboard/SKILL.md
Normal file
138
.claude/skills/add-dashboard/SKILL.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: add-dashboard
|
||||
description: Add a monitoring dashboard to NanoClaw. Installs @nanoco/nanoclaw-dashboard and a pusher that sends periodic JSON snapshots.
|
||||
---
|
||||
|
||||
# /add-dashboard — NanoClaw Dashboard
|
||||
|
||||
Adds a local monitoring dashboard showing agent groups, sessions, channels, users, token usage, context windows, message activity, and real-time logs.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
NanoClaw (pusher) Dashboard (npm package)
|
||||
┌──────────┐ POST JSON ┌──────────────┐
|
||||
│ collects │ ────────────────→ │ /api/ingest │
|
||||
│ DB data │ every 60s │ in-memory │
|
||||
│ tails │ ────────────────→ │ /api/logs/ │
|
||||
│ log file │ every 2s │ push │
|
||||
└──────────┘ │ serves UI │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Install the npm package
|
||||
|
||||
```bash
|
||||
pnpm install @nanoco/nanoclaw-dashboard
|
||||
```
|
||||
|
||||
### 2. Copy the pusher module
|
||||
|
||||
Copy the resource file into src:
|
||||
|
||||
```
|
||||
.claude/skills/add-dashboard/resources/dashboard-pusher.ts → src/dashboard-pusher.ts
|
||||
```
|
||||
|
||||
### 3. Add exports to src/db/index.ts
|
||||
|
||||
Add these two export blocks if not already present:
|
||||
|
||||
```typescript
|
||||
// After the messaging-groups exports, add:
|
||||
export {
|
||||
getMessagingGroupsByAgentGroup,
|
||||
} from './messaging-groups.js';
|
||||
|
||||
// Before the credentials exports, add:
|
||||
export {
|
||||
createDestination,
|
||||
getDestinations,
|
||||
getDestinationByName,
|
||||
getDestinationByTarget,
|
||||
hasDestination,
|
||||
deleteDestination,
|
||||
} from './agent-destinations.js';
|
||||
```
|
||||
|
||||
### 4. Wire into src/index.ts
|
||||
|
||||
Add the `readEnvFile` import at the top if not already present:
|
||||
|
||||
```typescript
|
||||
import { readEnvFile } from './env.js';
|
||||
```
|
||||
|
||||
Add after step 7 (OneCLI approval handler), before the `log.info('NanoClaw running')` line:
|
||||
|
||||
```typescript
|
||||
// 8. Dashboard (optional)
|
||||
const dashboardEnv = readEnvFile(['DASHBOARD_SECRET', 'DASHBOARD_PORT']);
|
||||
const dashboardSecret = process.env.DASHBOARD_SECRET || dashboardEnv.DASHBOARD_SECRET;
|
||||
const dashboardPort = parseInt(process.env.DASHBOARD_PORT || dashboardEnv.DASHBOARD_PORT || '3100', 10);
|
||||
if (dashboardSecret) {
|
||||
const { startDashboard } = await import('@nanoco/nanoclaw-dashboard');
|
||||
const { startDashboardPusher } = await import('./dashboard-pusher.js');
|
||||
startDashboard({ port: dashboardPort, secret: dashboardSecret });
|
||||
startDashboardPusher({ port: dashboardPort, secret: dashboardSecret, intervalMs: 60000 });
|
||||
} else {
|
||||
log.info('Dashboard disabled (no DASHBOARD_SECRET)');
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add environment variables to .env
|
||||
|
||||
```
|
||||
DASHBOARD_SECRET=<generate-a-random-secret>
|
||||
DASHBOARD_PORT=3100
|
||||
```
|
||||
|
||||
Generate the secret: `node -e "console.log('nc-' + require('crypto').randomBytes(16).toString('hex'))"`
|
||||
|
||||
### 6. Build and restart
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
systemctl --user restart nanoclaw # Linux
|
||||
# or: launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
```
|
||||
|
||||
### 7. Verify
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:3100/api/status
|
||||
curl -s -H "Authorization: Bearer <secret>" http://localhost:3100/api/overview
|
||||
```
|
||||
|
||||
Open `http://localhost:3100/dashboard` in a browser.
|
||||
|
||||
## Dashboard Pages
|
||||
|
||||
| Page | Shows |
|
||||
|------|-------|
|
||||
| Overview | Stats, token usage + cache hit rate, context windows, activity chart |
|
||||
| Agent Groups | Sessions, wirings, destinations, members, admins |
|
||||
| Sessions | Status, container state, context window usage bars |
|
||||
| Channels | Live/offline status, messaging groups, sender policies |
|
||||
| Messages | Per-session inbound/outbound messages |
|
||||
| Users | Privilege hierarchy: owner > admin > member |
|
||||
| Logs | Real-time log streaming with level filter |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"No data yet"**: Wait 60s for first push, or check logs for push errors
|
||||
- **401 errors**: Verify `DASHBOARD_SECRET` matches in `.env`
|
||||
- **Port conflict**: Change `DASHBOARD_PORT` in `.env`
|
||||
- **No logs**: Check `logs/nanoclaw.log` exists
|
||||
|
||||
## Removal
|
||||
|
||||
```bash
|
||||
pnpm uninstall @nanoco/nanoclaw-dashboard
|
||||
rm src/dashboard-pusher.ts
|
||||
# Remove the dashboard block from src/index.ts
|
||||
# Remove DASHBOARD_SECRET and DASHBOARD_PORT from .env
|
||||
pnpm run build
|
||||
```
|
||||
495
.claude/skills/add-dashboard/resources/dashboard-pusher.ts
Normal file
495
.claude/skills/add-dashboard/resources/dashboard-pusher.ts
Normal file
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* Dashboard pusher — collects NanoClaw state and POSTs a JSON
|
||||
* snapshot to the dashboard's /api/ingest endpoint every interval.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import http from 'http';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
import { getAllAgentGroups, getAgentGroup } from './db/agent-groups.js';
|
||||
import { getSessionsByAgentGroup } from './db/sessions.js';
|
||||
import { getAllMessagingGroups, getMessagingGroupAgents } from './db/messaging-groups.js';
|
||||
import { getDestinations } from './db/agent-destinations.js';
|
||||
import { getMembers } from './db/agent-group-members.js';
|
||||
import { getAllUsers, getUser } from './db/users.js';
|
||||
import { getUserRoles, getAdminsOfAgentGroup } from './db/user-roles.js';
|
||||
import { getUserDmsForUser } from './db/user-dms.js';
|
||||
import { getActiveAdapters, getRegisteredChannelNames } from './channels/channel-registry.js';
|
||||
import { DATA_DIR, ASSISTANT_NAME } from './config.js';
|
||||
import { getDb } from './db/connection.js';
|
||||
import { log } from './log.js';
|
||||
|
||||
interface PusherConfig {
|
||||
port: number;
|
||||
secret: string;
|
||||
intervalMs?: number;
|
||||
}
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
let logTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let logOffset = 0;
|
||||
|
||||
export function startDashboardPusher(config: PusherConfig): void {
|
||||
const interval = config.intervalMs || 60000;
|
||||
|
||||
// Push immediately on start, then on interval
|
||||
push(config).catch((err) => log.error('Dashboard push failed', { err }));
|
||||
timer = setInterval(() => {
|
||||
push(config).catch((err) => log.error('Dashboard push failed', { err }));
|
||||
}, interval);
|
||||
|
||||
// Start log file tailing
|
||||
startLogTail(config);
|
||||
|
||||
log.info('Dashboard pusher started', { intervalMs: interval });
|
||||
}
|
||||
|
||||
export function stopDashboardPusher(): void {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
if (logTimer) {
|
||||
clearInterval(logTimer);
|
||||
logTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fire-and-forget POST to the dashboard. */
|
||||
function postJson(config: PusherConfig, urlPath: string, data: unknown): void {
|
||||
const body = JSON.stringify(data);
|
||||
const req = http.request({
|
||||
hostname: '127.0.0.1',
|
||||
port: config.port,
|
||||
path: urlPath,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
Authorization: `Bearer ${config.secret}`,
|
||||
},
|
||||
});
|
||||
req.on('error', () => {});
|
||||
req.write(body);
|
||||
req.end();
|
||||
}
|
||||
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
function startLogTail(config: PusherConfig): void {
|
||||
const logFile = path.resolve(process.cwd(), 'logs', 'nanoclaw.log');
|
||||
if (!fs.existsSync(logFile)) return;
|
||||
|
||||
// Send last 200 lines as backfill
|
||||
try {
|
||||
const allLines = fs.readFileSync(logFile, 'utf-8').split('\n').filter((l) => l.trim());
|
||||
logOffset = fs.statSync(logFile).size;
|
||||
const tail = allLines.slice(-200).map((l) => l.replace(ANSI_RE, ''));
|
||||
if (tail.length > 0) postJson(config, '/api/logs/push', { lines: tail });
|
||||
} catch { return; }
|
||||
|
||||
// Poll every 2s for new lines
|
||||
logTimer = setInterval(() => {
|
||||
try {
|
||||
const stat = fs.statSync(logFile);
|
||||
if (stat.size <= logOffset) { logOffset = stat.size; return; }
|
||||
const buf = Buffer.alloc(stat.size - logOffset);
|
||||
const fd = fs.openSync(logFile, 'r');
|
||||
fs.readSync(fd, buf, 0, buf.length, logOffset);
|
||||
fs.closeSync(fd);
|
||||
logOffset = stat.size;
|
||||
const lines = buf.toString().split('\n').filter((l) => l.trim()).map((l) => l.replace(ANSI_RE, ''));
|
||||
if (lines.length > 0) postJson(config, '/api/logs/push', { lines });
|
||||
} catch { /* ignore */ }
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async function push(config: PusherConfig): Promise<void> {
|
||||
const snapshot = collectSnapshot();
|
||||
postJson(config, '/api/ingest', snapshot);
|
||||
log.debug('Dashboard snapshot pushed');
|
||||
}
|
||||
|
||||
function collectSnapshot(): Record<string, unknown> {
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
assistant_name: ASSISTANT_NAME,
|
||||
uptime: Math.floor(process.uptime()),
|
||||
agent_groups: collectAgentGroups(),
|
||||
sessions: collectSessions(),
|
||||
channels: collectChannels(),
|
||||
users: collectUsers(),
|
||||
tokens: collectTokens(),
|
||||
context_windows: collectContextWindows(),
|
||||
activity: collectActivity(),
|
||||
messages: collectMessages(),
|
||||
};
|
||||
}
|
||||
|
||||
function collectAgentGroups() {
|
||||
return getAllAgentGroups().map((g) => {
|
||||
const sessions = getSessionsByAgentGroup(g.id);
|
||||
const running = sessions.filter((s) => s.container_status === 'running' || s.container_status === 'idle');
|
||||
const destinations = getDestinations(g.id);
|
||||
const members = getMembers(g.id).map((m) => {
|
||||
const user = getUser(m.user_id);
|
||||
return { ...m, display_name: user?.display_name ?? null };
|
||||
});
|
||||
const admins = getAdminsOfAgentGroup(g.id).map((a) => {
|
||||
const user = getUser(a.user_id);
|
||||
return { ...a, display_name: user?.display_name ?? null };
|
||||
});
|
||||
|
||||
// Wirings
|
||||
const db = getDb();
|
||||
const wirings = db
|
||||
.prepare(
|
||||
`SELECT mga.*, mg.channel_type, mg.platform_id, mg.name as mg_name, mg.is_group, mg.unknown_sender_policy
|
||||
FROM messaging_group_agents mga
|
||||
JOIN messaging_groups mg ON mg.id = mga.messaging_group_id
|
||||
WHERE mga.agent_group_id = ?`,
|
||||
)
|
||||
.all(g.id) as Array<Record<string, unknown>>;
|
||||
|
||||
return {
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
folder: g.folder,
|
||||
agent_provider: g.agent_provider,
|
||||
container_config: g.container_config ? JSON.parse(g.container_config) : null,
|
||||
sessionCount: sessions.length,
|
||||
runningSessions: running.length,
|
||||
wirings,
|
||||
destinations,
|
||||
members,
|
||||
admins,
|
||||
created_at: g.created_at,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function collectSessions() {
|
||||
const db = getDb();
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT s.*, ag.name as agent_group_name, ag.folder as agent_group_folder,
|
||||
mg.channel_type, mg.platform_id, mg.name as messaging_group_name
|
||||
FROM sessions s
|
||||
LEFT JOIN agent_groups ag ON ag.id = s.agent_group_id
|
||||
LEFT JOIN messaging_groups mg ON mg.id = s.messaging_group_id
|
||||
ORDER BY s.last_active DESC NULLS LAST`,
|
||||
)
|
||||
.all() as Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
function collectChannels() {
|
||||
const messagingGroups = getAllMessagingGroups();
|
||||
const liveAdapters = getActiveAdapters().map((a) => a.channelType);
|
||||
const registeredChannels = getRegisteredChannelNames();
|
||||
|
||||
const byType: Record<string, { channelType: string; isLive: boolean; isRegistered: boolean; groups: unknown[] }> = {};
|
||||
|
||||
for (const mg of messagingGroups) {
|
||||
if (!byType[mg.channel_type]) {
|
||||
byType[mg.channel_type] = {
|
||||
channelType: mg.channel_type,
|
||||
isLive: liveAdapters.includes(mg.channel_type),
|
||||
isRegistered: registeredChannels.includes(mg.channel_type),
|
||||
groups: [],
|
||||
};
|
||||
}
|
||||
|
||||
const agents = getMessagingGroupAgents(mg.id).map((a) => {
|
||||
const group = getAgentGroup(a.agent_group_id);
|
||||
return { agent_group_id: a.agent_group_id, agent_group_name: group?.name ?? null, priority: a.priority };
|
||||
});
|
||||
|
||||
byType[mg.channel_type].groups.push({
|
||||
messagingGroup: {
|
||||
id: mg.id,
|
||||
platform_id: mg.platform_id,
|
||||
name: mg.name,
|
||||
is_group: mg.is_group,
|
||||
unknown_sender_policy: (mg as unknown as Record<string, unknown>).unknown_sender_policy ?? 'strict',
|
||||
},
|
||||
agents,
|
||||
});
|
||||
}
|
||||
|
||||
// Include live adapters with no messaging groups
|
||||
for (const ct of liveAdapters) {
|
||||
if (!byType[ct]) {
|
||||
byType[ct] = { channelType: ct, isLive: true, isRegistered: true, groups: [] };
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(byType).sort((a, b) => a.channelType.localeCompare(b.channelType));
|
||||
}
|
||||
|
||||
function collectUsers() {
|
||||
return getAllUsers().map((u) => {
|
||||
const roles = getUserRoles(u.id);
|
||||
const dms = getUserDmsForUser(u.id);
|
||||
|
||||
const db = getDb();
|
||||
const memberships = db
|
||||
.prepare(
|
||||
`SELECT agm.agent_group_id, ag.name as agent_group_name
|
||||
FROM agent_group_members agm
|
||||
JOIN agent_groups ag ON ag.id = agm.agent_group_id
|
||||
WHERE agm.user_id = ?`,
|
||||
)
|
||||
.all(u.id) as Array<Record<string, unknown>>;
|
||||
|
||||
let privilege = 'none';
|
||||
if (roles.some((r) => r.role === 'owner')) privilege = 'owner';
|
||||
else if (roles.some((r) => r.role === 'admin' && !r.agent_group_id)) privilege = 'global_admin';
|
||||
else if (roles.some((r) => r.role === 'admin')) privilege = 'admin';
|
||||
else if (memberships.length > 0) privilege = 'member';
|
||||
|
||||
return {
|
||||
id: u.id,
|
||||
kind: u.kind,
|
||||
display_name: u.display_name,
|
||||
privilege,
|
||||
roles,
|
||||
memberships,
|
||||
dmChannels: dms.map((d) => ({ channel_type: d.channel_type })),
|
||||
created_at: u.created_at,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function collectTokens() {
|
||||
const sessionsDir = path.join(DATA_DIR, 'v2-sessions');
|
||||
const allEntries: Array<{ model: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number; agentGroupId: string }> = [];
|
||||
const agentGroups = getAllAgentGroups();
|
||||
const nameMap = new Map(agentGroups.map((g) => [g.id, g.name]));
|
||||
|
||||
if (fs.existsSync(sessionsDir)) {
|
||||
for (const agDir of fs.readdirSync(sessionsDir).filter((d) => d.startsWith('ag-'))) {
|
||||
const entries = scanJsonlTokens(path.join(sessionsDir, agDir));
|
||||
allEntries.push(...entries.map((e) => ({ ...e, agentGroupId: agDir })));
|
||||
}
|
||||
}
|
||||
|
||||
const byModel: Record<string, { requests: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number }> = {};
|
||||
const byGroup: Record<string, { requests: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number; name: string }> = {};
|
||||
const totals = { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 };
|
||||
|
||||
for (const e of allEntries) {
|
||||
if (!byModel[e.model]) byModel[e.model] = { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 };
|
||||
byModel[e.model].requests++;
|
||||
byModel[e.model].inputTokens += e.inputTokens;
|
||||
byModel[e.model].outputTokens += e.outputTokens;
|
||||
byModel[e.model].cacheReadTokens += e.cacheReadTokens;
|
||||
byModel[e.model].cacheCreationTokens += e.cacheCreationTokens;
|
||||
|
||||
if (!byGroup[e.agentGroupId]) byGroup[e.agentGroupId] = { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, name: nameMap.get(e.agentGroupId) || e.agentGroupId };
|
||||
byGroup[e.agentGroupId].requests++;
|
||||
byGroup[e.agentGroupId].inputTokens += e.inputTokens;
|
||||
byGroup[e.agentGroupId].outputTokens += e.outputTokens;
|
||||
byGroup[e.agentGroupId].cacheReadTokens += e.cacheReadTokens;
|
||||
byGroup[e.agentGroupId].cacheCreationTokens += e.cacheCreationTokens;
|
||||
|
||||
totals.requests++;
|
||||
totals.inputTokens += e.inputTokens;
|
||||
totals.outputTokens += e.outputTokens;
|
||||
totals.cacheReadTokens += e.cacheReadTokens;
|
||||
totals.cacheCreationTokens += e.cacheCreationTokens;
|
||||
}
|
||||
|
||||
return { totals, byModel, byGroup };
|
||||
}
|
||||
|
||||
function scanJsonlTokens(agentDir: string) {
|
||||
const claudeDir = path.join(agentDir, '.claude-shared', 'projects');
|
||||
if (!fs.existsSync(claudeDir)) return [];
|
||||
|
||||
const entries: Array<{ model: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number }> = [];
|
||||
|
||||
const walk = (dir: string): void => {
|
||||
try {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(full);
|
||||
else if (entry.name.endsWith('.jsonl')) {
|
||||
try {
|
||||
for (const line of fs.readFileSync(full, 'utf-8').split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const r = JSON.parse(line);
|
||||
if (r.type === 'assistant' && r.message?.usage) {
|
||||
const u = r.message.usage;
|
||||
entries.push({
|
||||
model: r.message.model || 'unknown',
|
||||
inputTokens: u.input_tokens || 0,
|
||||
outputTokens: u.output_tokens || 0,
|
||||
cacheReadTokens: u.cache_read_input_tokens || 0,
|
||||
cacheCreationTokens: u.cache_creation_input_tokens || 0,
|
||||
});
|
||||
}
|
||||
} catch { /* skip line */ }
|
||||
}
|
||||
} catch { /* skip file */ }
|
||||
}
|
||||
}
|
||||
} catch { /* skip dir */ }
|
||||
};
|
||||
walk(claudeDir);
|
||||
return entries;
|
||||
}
|
||||
|
||||
function collectContextWindows() {
|
||||
const sessionsDir = path.join(DATA_DIR, 'v2-sessions');
|
||||
if (!fs.existsSync(sessionsDir)) return [];
|
||||
|
||||
const results: unknown[] = [];
|
||||
const agentGroups = getAllAgentGroups();
|
||||
const nameMap = new Map(agentGroups.map((g) => [g.id, g.name]));
|
||||
|
||||
for (const agDir of fs.readdirSync(sessionsDir).filter((d) => d.startsWith('ag-'))) {
|
||||
const claudeDir = path.join(sessionsDir, agDir, '.claude-shared', 'projects');
|
||||
if (!fs.existsSync(claudeDir)) continue;
|
||||
|
||||
// Find most recent JSONL
|
||||
const jsonlFiles: string[] = [];
|
||||
const walk = (dir: string): void => {
|
||||
try {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(full);
|
||||
else if (entry.name.endsWith('.jsonl')) jsonlFiles.push(full);
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
};
|
||||
walk(claudeDir);
|
||||
if (jsonlFiles.length === 0) continue;
|
||||
|
||||
jsonlFiles.sort((a, b) => {
|
||||
try { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; } catch { return 0; }
|
||||
});
|
||||
|
||||
// Read last assistant turn from newest file
|
||||
const content = fs.readFileSync(jsonlFiles[0], 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
if (!lines[i].trim()) continue;
|
||||
try {
|
||||
const r = JSON.parse(lines[i]);
|
||||
if (r.type === 'assistant' && r.message?.usage) {
|
||||
const u = r.message.usage;
|
||||
const model = r.message.model || 'unknown';
|
||||
const ctx = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
||||
const max = 200000;
|
||||
results.push({
|
||||
agentGroupId: agDir,
|
||||
agentGroupName: nameMap.get(agDir),
|
||||
sessionId: path.basename(jsonlFiles[0], '.jsonl'),
|
||||
model,
|
||||
contextTokens: ctx,
|
||||
outputTokens: u.output_tokens || 0,
|
||||
cacheReadTokens: u.cache_read_input_tokens || 0,
|
||||
cacheCreationTokens: u.cache_creation_input_tokens || 0,
|
||||
maxContext: max,
|
||||
usagePercent: max > 0 ? Math.round((ctx / max) * 100) : 0,
|
||||
timestamp: r.timestamp || '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function collectActivity() {
|
||||
const now = Date.now();
|
||||
const buckets: Record<string, { inbound: number; outbound: number }> = {};
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const key = new Date(now - i * 3600000).toISOString().slice(0, 13);
|
||||
buckets[key] = { inbound: 0, outbound: 0 };
|
||||
}
|
||||
|
||||
const sessionsDir = path.join(DATA_DIR, 'v2-sessions');
|
||||
if (!fs.existsSync(sessionsDir)) return toBucketArray(buckets);
|
||||
|
||||
const cutoff = new Date(now - 86400000).toISOString();
|
||||
|
||||
try {
|
||||
for (const agDir of fs.readdirSync(sessionsDir).filter((d) => d.startsWith('ag-'))) {
|
||||
const agPath = path.join(sessionsDir, agDir);
|
||||
for (const sessDir of fs.readdirSync(agPath).filter((d) => d.startsWith('sess-'))) {
|
||||
for (const [dbName, direction] of [['outbound.db', 'outbound'], ['inbound.db', 'inbound']] as const) {
|
||||
const dbPath = path.join(agPath, sessDir, dbName);
|
||||
if (!fs.existsSync(dbPath)) continue;
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const table = direction === 'outbound' ? 'messages_out' : 'messages_in';
|
||||
const rows = db.prepare(`SELECT timestamp FROM ${table} WHERE timestamp > ?`).all(cutoff) as { timestamp: string }[];
|
||||
for (const row of rows) {
|
||||
const key = row.timestamp.slice(0, 13);
|
||||
if (buckets[key]) buckets[key][direction]++;
|
||||
}
|
||||
db.close();
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
|
||||
return toBucketArray(buckets);
|
||||
}
|
||||
|
||||
function toBucketArray(buckets: Record<string, { inbound: number; outbound: number }>) {
|
||||
return Object.entries(buckets)
|
||||
.map(([hour, counts]) => ({ hour, ...counts }))
|
||||
.sort((a, b) => a.hour.localeCompare(b.hour));
|
||||
}
|
||||
|
||||
function collectMessages() {
|
||||
const sessionsDir = path.join(DATA_DIR, 'v2-sessions');
|
||||
if (!fs.existsSync(sessionsDir)) return [];
|
||||
|
||||
const results: Array<{ agentGroupId: string; sessionId: string; inbound: unknown[]; outbound: unknown[] }> = [];
|
||||
const limit = 50;
|
||||
|
||||
try {
|
||||
for (const agDir of fs.readdirSync(sessionsDir).filter((d) => d.startsWith('ag-'))) {
|
||||
const agPath = path.join(sessionsDir, agDir);
|
||||
for (const sessDir of fs.readdirSync(agPath).filter((d) => d.startsWith('sess-'))) {
|
||||
const inbound: unknown[] = [];
|
||||
const outbound: unknown[] = [];
|
||||
|
||||
const inDbPath = path.join(agPath, sessDir, 'inbound.db');
|
||||
if (fs.existsSync(inDbPath)) {
|
||||
try {
|
||||
const db = new Database(inDbPath, { readonly: true });
|
||||
const rows = db.prepare('SELECT * FROM messages_in ORDER BY seq DESC LIMIT ?').all(limit);
|
||||
inbound.push(...(rows as unknown[]).reverse());
|
||||
db.close();
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
const outDbPath = path.join(agPath, sessDir, 'outbound.db');
|
||||
if (fs.existsSync(outDbPath)) {
|
||||
try {
|
||||
const db = new Database(outDbPath, { readonly: true });
|
||||
const rows = db.prepare('SELECT * FROM messages_out ORDER BY seq DESC LIMIT ?').all(limit);
|
||||
outbound.push(...(rows as unknown[]).reverse());
|
||||
db.close();
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
if (inbound.length > 0 || outbound.length > 0) {
|
||||
results.push({ agentGroupId: agDir, sessionId: sessDir, inbound, outbound });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
|
||||
return results;
|
||||
}
|
||||
7
.claude/skills/add-discord/REMOVE.md
Normal file
7
.claude/skills/add-discord/REMOVE.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Remove Discord
|
||||
|
||||
1. Comment out `import './discord.js'` in `src/channels/index.ts`
|
||||
2. Remove `DISCORD_BOT_TOKEN` from `.env`
|
||||
3. Rebuild and restart
|
||||
|
||||
No package to uninstall — Discord is built in.
|
||||
@@ -1,203 +1,98 @@
|
||||
---
|
||||
name: add-discord
|
||||
description: Add Discord bot channel integration to NanoClaw.
|
||||
description: Add Discord bot channel integration via Chat SDK.
|
||||
---
|
||||
|
||||
# Add Discord Channel
|
||||
|
||||
This skill adds Discord support to NanoClaw, then walks through interactive setup.
|
||||
Adds Discord bot support via the Chat SDK bridge.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
## Install
|
||||
|
||||
### Check if already applied
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Discord adapter in from the `channels` branch.
|
||||
|
||||
Check if `src/channels/discord.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
### Ask the user
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
Use `AskUserQuestion` to collect configuration:
|
||||
- `src/channels/discord.ts` exists
|
||||
- `src/channels/index.ts` contains `import './discord.js';`
|
||||
- `@chat-adapter/discord` is listed in `package.json` dependencies
|
||||
|
||||
AskUserQuestion: Do you have a Discord bot token, or do you need to create one?
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
If they have one, collect it now. If not, we'll create one in Phase 3.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure channel remote
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
If `discord` is missing, add it:
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git remote add discord https://github.com/qwibitai/nanoclaw-discord.git
|
||||
git show origin/channels:src/channels/discord.ts > src/channels/discord.ts
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './discord.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
git fetch discord main
|
||||
git merge discord/main || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
pnpm install @chat-adapter/discord@4.26.0
|
||||
```
|
||||
|
||||
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
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/channels/discord.test.ts
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
All tests must pass (including the new Discord tests) and build must be clean before proceeding.
|
||||
## Credentials
|
||||
|
||||
## Phase 3: Setup
|
||||
### Create Discord Bot
|
||||
|
||||
### 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.
|
||||
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
2. Click **New Application** and give it a name (e.g., "NanoClaw Assistant")
|
||||
3. From the **General Information** tab, copy the **Application ID** and **Public Key**
|
||||
4. Go to the **Bot** tab and click **Add Bot** if needed
|
||||
5. Copy the Bot Token (click **Reset Token** if you need a new one — you can only see it once)
|
||||
6. Under **Privileged Gateway Intents**, enable **Message Content Intent**
|
||||
7. Go to **OAuth2** > **URL Generator**:
|
||||
- Scopes: select `bot`
|
||||
- Bot Permissions: select `Send Messages`, `Read Message History`, `Add Reactions`, `Attach Files`, `Use Slash Commands`
|
||||
8. Copy the generated URL and open it in your browser to invite the bot to your server
|
||||
|
||||
### Configure environment
|
||||
|
||||
All three values are required — the adapter will fail to start without `DISCORD_PUBLIC_KEY` and `DISCORD_APPLICATION_ID`.
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
DISCORD_BOT_TOKEN=<their-token>
|
||||
DISCORD_BOT_TOKEN=your-bot-token
|
||||
DISCORD_APPLICATION_ID=your-application-id
|
||||
DISCORD_PUBLIC_KEY=your-public-key
|
||||
```
|
||||
|
||||
Channels auto-enable when their credentials are present — no extra configuration needed.
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
Sync to container environment:
|
||||
## Next Steps
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
```
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
The container reads environment from `data/env/env`, not `.env` directly.
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
### Build and restart
|
||||
## Channel Info
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
The channel ID, name, and folder name are needed. Use `npx tsx setup/index.ts --step register` with the appropriate flags.
|
||||
|
||||
For a main channel (responds to all messages):
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step register -- --jid "dc:<channel-id>" --name "<server-name> #<channel-name>" --folder "discord_main" --trigger "@${ASSISTANT_NAME}" --channel discord --no-trigger-required --is-main
|
||||
```
|
||||
|
||||
For additional channels (trigger-only):
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step register -- --jid "dc:<channel-id>" --name "<server-name> #<channel-name>" --folder "discord_<channel-name>" --trigger "@${ASSISTANT_NAME}" --channel discord
|
||||
```
|
||||
|
||||
## 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
|
||||
- **type**: `discord`
|
||||
- **terminology**: Discord has "servers" (also called "guilds") containing "channels." Text channels start with #. The bot can also receive direct messages.
|
||||
- **how-to-find-id**: Enable Developer Mode in Discord (Settings > App Settings > Advanced > Developer Mode). Then right-click a server and select "Copy Server ID" for the guild ID, and right-click the text channel and select "Copy Channel ID." The platform ID format used in registration is `discord:{guildId}:{channelId}` — both IDs are required.
|
||||
- **supports-threads**: yes
|
||||
- **typical-use**: Interactive chat — server channels or direct messages
|
||||
- **default-isolation**: Same agent group for your personal server. Separate agent group for servers with different communities or where different members have different information boundaries.
|
||||
|
||||
3
.claude/skills/add-discord/VERIFY.md
Normal file
3
.claude/skills/add-discord/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify Discord
|
||||
|
||||
Send a message in a channel where the bot has access, or DM the bot directly. The bot should respond within a few seconds.
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
name: add-emacs
|
||||
description: Add Emacs as a channel. Opens an interactive chat buffer and org-mode integration so you can talk to NanoClaw from within Emacs (Doom, Spacemacs, or vanilla). Uses a local HTTP bridge — no bot token or external service needed.
|
||||
description: Add Emacs as a channel. Opens an interactive chat buffer and org-mode integration so you can talk to NanoClaw from within Emacs (Doom, Spacemacs, or vanilla). Local HTTP bridge — no bot token or external service needed.
|
||||
---
|
||||
|
||||
# Add Emacs Channel
|
||||
|
||||
This skill adds Emacs support to NanoClaw, then walks through interactive setup.
|
||||
Works with Doom Emacs, Spacemacs, and vanilla Emacs 27.1+.
|
||||
Adds Emacs support via a local HTTP bridge. Works with Doom Emacs, Spacemacs, and vanilla Emacs 27.1+.
|
||||
|
||||
## What you can do with this
|
||||
|
||||
@@ -15,95 +14,99 @@ Works with Doom Emacs, Spacemacs, and vanilla Emacs 27.1+.
|
||||
- **Meeting notes** — send an org agenda entry; get a summary or action item list back as a child node
|
||||
- **Draft writing** — send org prose; receive revisions or continuations in place
|
||||
- **Research capture** — ask a question directly in your org notes; the answer lands exactly where you need it
|
||||
- **Schedule tasks** — ask Andy to set a reminder or create a scheduled NanoClaw task (e.g. "remind me tomorrow to review the PR")
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
## Install
|
||||
|
||||
### Check if already applied
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Emacs adapter and the Lisp client in from the `channels` branch. Native HTTP bridge — no Chat SDK, no adapter package.
|
||||
|
||||
Check if `src/channels/emacs.ts` exists:
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Enable** if all of these are already in place:
|
||||
|
||||
- `src/channels/emacs.ts` exists
|
||||
- `emacs/nanoclaw.el` exists
|
||||
- `src/channels/index.ts` contains `import './emacs.js';`
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
test -f src/channels/emacs.ts && echo "already applied" || echo "not applied"
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
If it exists, skip to Phase 3 (Setup). The code changes are already in place.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure the upstream remote
|
||||
### 2. Copy the adapter and Lisp client
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
mkdir -p emacs
|
||||
git show origin/channels:src/channels/emacs.ts > src/channels/emacs.ts
|
||||
git show origin/channels:src/channels/emacs.test.ts > src/channels/emacs.test.ts
|
||||
git show origin/channels:emacs/nanoclaw.el > emacs/nanoclaw.el
|
||||
```
|
||||
|
||||
If an `upstream` remote pointing to `https://github.com/qwibitai/nanoclaw.git` is missing,
|
||||
add it:
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './emacs.js';
|
||||
```
|
||||
|
||||
### 4. Build
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
No npm package to install — the adapter uses only Node builtins (`http`).
|
||||
|
||||
## Enable
|
||||
|
||||
The adapter is gated by `EMACS_ENABLED` so the HTTP port isn't opened on hosts that aren't running Emacs. Add to `.env`:
|
||||
|
||||
```bash
|
||||
git fetch upstream skill/emacs
|
||||
git merge upstream/skill/emacs
|
||||
EMACS_ENABLED=true
|
||||
EMACS_CHANNEL_PORT=8766 # optional — change only if 8766 is taken
|
||||
EMACS_AUTH_TOKEN= # optional — set to a random string to lock the endpoint
|
||||
EMACS_PLATFORM_ID=default # optional — only change if you want a non-default chat id
|
||||
```
|
||||
|
||||
If there are merge conflicts on `package-lock.json`, resolve them by accepting the incoming
|
||||
version and continuing:
|
||||
Generate an auth token (recommended even on single-user machines — prevents other local processes from poking the endpoint):
|
||||
|
||||
```bash
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
|
||||
```
|
||||
|
||||
For any other conflict, read the conflicted file and reconcile both sides manually.
|
||||
## Wire the channel
|
||||
|
||||
This adds:
|
||||
- `src/channels/emacs.ts` — `EmacsBridgeChannel` HTTP server (port 8766)
|
||||
- `src/channels/emacs.test.ts` — unit tests
|
||||
- `emacs/nanoclaw.el` — Emacs Lisp package (`nanoclaw-chat`, `nanoclaw-org-send`)
|
||||
- `import './emacs.js'` appended to `src/channels/index.ts`
|
||||
Emacs is a single-user, single-chat channel. One host = one messaging group with `platform_id = "default"`.
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
### If this is your first agent group
|
||||
|
||||
### Validate code changes
|
||||
Run `/init-first-agent` — pick **Emacs** as the channel, use any short handle as the "user id" (e.g. your OS username), and the skill will create the agent group, wire the channel, and write a welcome message that the agent delivers back to your Emacs buffer.
|
||||
|
||||
### Otherwise — wire to an existing agent group
|
||||
|
||||
Run the `register` step directly. The `EMACS_PLATFORM_ID` (default `default`) becomes the messaging group's platform id:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npx vitest run src/channels/emacs.test.ts
|
||||
pnpm exec tsx setup/index.ts --step register -- \
|
||||
--platform-id "default" --name "Emacs" \
|
||||
--folder "<existing-folder>" --channel "emacs" \
|
||||
--session-mode "agent-shared" \
|
||||
--assistant-name "<existing-assistant-name>"
|
||||
```
|
||||
|
||||
Build must be clean and tests must pass before proceeding.
|
||||
`agent-shared` puts Emacs messages in the same session as any other channel wired to the same agent group — so a conversation you started in Telegram continues in Emacs. Use `shared` to keep an independent Emacs thread with the same workspace, or a new `--folder` for a dedicated Emacs-only agent.
|
||||
|
||||
## Phase 3: Setup
|
||||
## Configure Emacs
|
||||
|
||||
### Configure environment (optional)
|
||||
|
||||
The channel works out of the box with defaults. Add to `.env` only if you need non-defaults:
|
||||
|
||||
```bash
|
||||
EMACS_CHANNEL_PORT=8766 # default — change if 8766 is already in use
|
||||
EMACS_AUTH_TOKEN=<random> # optional — locks the endpoint to Emacs only
|
||||
```
|
||||
|
||||
If you change or add values, sync to the container environment:
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
```
|
||||
|
||||
### Configure Emacs
|
||||
|
||||
The `nanoclaw.el` package requires only Emacs 27.1+ built-in libraries (`url`, `json`, `org`) — no package manager setup needed.
|
||||
`nanoclaw.el` needs only Emacs 27.1+ builtins (`url`, `json`, `org`) — no package manager.
|
||||
|
||||
AskUserQuestion: Which Emacs distribution are you using?
|
||||
- **Doom Emacs** - config.el with map! keybindings
|
||||
- **Spacemacs** - dotspacemacs/user-config in ~/.spacemacs
|
||||
- **Vanilla Emacs / other** - init.el with global-set-key
|
||||
- **Doom Emacs** — `config.el` with `map!` keybindings
|
||||
- **Spacemacs** — `dotspacemacs/user-config` in `~/.spacemacs`
|
||||
- **Vanilla Emacs / other** — `init.el` with `global-set-key`
|
||||
|
||||
**Doom Emacs** — add to `~/.config/doom/config.el` (or `~/.doom.d/config.el`):
|
||||
|
||||
@@ -117,7 +120,7 @@ AskUserQuestion: Which Emacs distribution are you using?
|
||||
:desc "Send org" "o" #'nanoclaw-org-send)
|
||||
```
|
||||
|
||||
Then reload: `M-x doom/reload`
|
||||
Reload: `M-x doom/reload`
|
||||
|
||||
**Spacemacs** — add to `dotspacemacs/user-config` in `~/.spacemacs`:
|
||||
|
||||
@@ -129,9 +132,9 @@ Then reload: `M-x doom/reload`
|
||||
(spacemacs/set-leader-keys "aNo" #'nanoclaw-org-send)
|
||||
```
|
||||
|
||||
Then reload: `M-x dotspacemacs/sync-configuration-layers` or restart Emacs.
|
||||
Reload: `M-x dotspacemacs/sync-configuration-layers` or restart Emacs.
|
||||
|
||||
**Vanilla Emacs** — add to `~/.emacs.d/init.el` (or `~/.emacs`):
|
||||
**Vanilla Emacs** — add to `~/.emacs.d/init.el`:
|
||||
|
||||
```elisp
|
||||
;; NanoClaw — personal AI assistant channel
|
||||
@@ -141,61 +144,75 @@ Then reload: `M-x dotspacemacs/sync-configuration-layers` or restart Emacs.
|
||||
(global-set-key (kbd "C-c n o") #'nanoclaw-org-send)
|
||||
```
|
||||
|
||||
Then reload: `M-x eval-buffer` or restart Emacs.
|
||||
Reload: `M-x eval-buffer` or restart Emacs.
|
||||
|
||||
If `EMACS_AUTH_TOKEN` was set, also add (any distribution):
|
||||
Replace `~/src/nanoclaw/emacs/nanoclaw.el` with your actual NanoClaw checkout path.
|
||||
|
||||
If `EMACS_AUTH_TOKEN` is set, also add (any distribution):
|
||||
|
||||
```elisp
|
||||
(setq nanoclaw-auth-token "<your-token>")
|
||||
```
|
||||
|
||||
If `EMACS_CHANNEL_PORT` was changed from the default, also add:
|
||||
If you changed `EMACS_CHANNEL_PORT` from the default:
|
||||
|
||||
```elisp
|
||||
(setq nanoclaw-port <your-port>)
|
||||
```
|
||||
|
||||
### Restart NanoClaw
|
||||
## Restart NanoClaw
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# systemctl --user restart nanoclaw # Linux
|
||||
```
|
||||
|
||||
## Phase 4: Verify
|
||||
## Verify
|
||||
|
||||
### Test the HTTP endpoint
|
||||
### HTTP endpoint
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8766/api/messages?since=0"
|
||||
curl -s http://localhost:8766/api/messages?since=0
|
||||
```
|
||||
|
||||
Expected: `{"messages":[]}`
|
||||
|
||||
If you set `EMACS_AUTH_TOKEN`:
|
||||
Expected: `{"messages":[]}`. With an auth token:
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: Bearer <token>" "http://localhost:8766/api/messages?since=0"
|
||||
curl -s -H "Authorization: Bearer <token>" http://localhost:8766/api/messages?since=0
|
||||
```
|
||||
|
||||
### Test from Emacs
|
||||
### From Emacs
|
||||
|
||||
Tell the user:
|
||||
|
||||
> 1. Open the chat buffer with your keybinding (`SPC N c`, `SPC a N c`, or `C-c n c`)
|
||||
> 2. Type a message and press `RET`
|
||||
> 3. A response from Andy should appear within a few seconds
|
||||
> 2. Type a message and press `C-c C-c` to send (RET inserts newlines)
|
||||
> 3. A response should appear within a few seconds
|
||||
>
|
||||
> For org-mode: open any `.org` file, position the cursor on a heading, and use `SPC N o` / `SPC a N o` / `C-c n o`
|
||||
|
||||
### Check logs if needed
|
||||
### Log line
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log
|
||||
```
|
||||
`tail -f logs/nanoclaw.log` should show `Emacs channel listening` at startup.
|
||||
|
||||
Look for `Emacs channel listening` at startup and `Emacs message received` when a message is sent.
|
||||
## Channel Info
|
||||
|
||||
- **type**: `emacs`
|
||||
- **terminology**: Single local buffer. There are no "groups" or separate chats — one host = one chat, addressed by a `platform_id` string (default `default`).
|
||||
- **how-to-find-id**: The platform id is whatever you set in `EMACS_PLATFORM_ID` (default `default`). User handles are arbitrary; your OS username or first name is fine (e.g. `emacs:<username>`).
|
||||
- **supports-threads**: no
|
||||
- **typical-use**: Single developer talking to the assistant from within Emacs, alongside whatever other channel they use (Slack, Telegram, Discord).
|
||||
- **default-isolation**: Same agent group as the primary DM, with `session-mode = agent-shared` so a conversation started elsewhere continues in Emacs. Pick a separate folder only if you specifically want an Emacs-only persona.
|
||||
|
||||
### Features
|
||||
|
||||
- Interactive chat buffer (`nanoclaw-chat`) with markdown → org-mode rendering
|
||||
- Org integration (`nanoclaw-org-send`) — sends the current subtree or region; reply lands as a child heading
|
||||
- Optional bearer-token auth for the local endpoint
|
||||
- Single-user: the adapter exposes exactly one messaging group per host
|
||||
|
||||
Not applicable (design): multi-user channels, threads, cold DM initiation, typing indicators, attachments.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -205,66 +222,53 @@ Look for `Emacs channel listening` at startup and `Emacs message received` when
|
||||
Error: listen EADDRINUSE: address already in use :::8766
|
||||
```
|
||||
|
||||
Either a stale NanoClaw process is running, or 8766 is taken by another app.
|
||||
|
||||
Find and kill the stale process:
|
||||
Either a stale NanoClaw is running or another app has the port. Kill stale process or change port:
|
||||
|
||||
```bash
|
||||
lsof -ti :8766 | xargs kill -9
|
||||
# or set EMACS_CHANNEL_PORT in .env and mirror in Emacs config (nanoclaw-port)
|
||||
```
|
||||
|
||||
Or change the port in `.env` (`EMACS_CHANNEL_PORT=8767`) and update `nanoclaw-port` in Emacs config.
|
||||
### Adapter not starting
|
||||
|
||||
If `grep "Emacs channel listening" logs/nanoclaw.log` returns nothing, check that `EMACS_ENABLED=true` is in `.env` and that the adapter import is present:
|
||||
|
||||
```bash
|
||||
grep -q '^EMACS_ENABLED=true' .env && echo "enabled" || echo "not enabled"
|
||||
grep -q "import './emacs.js'" src/channels/index.ts && echo "imported" || echo "not imported"
|
||||
```
|
||||
|
||||
### No response from agent
|
||||
|
||||
Check:
|
||||
1. NanoClaw is running: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux)
|
||||
2. Emacs group is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid = 'emacs:default'"`
|
||||
3. Logs show activity: `tail -50 logs/nanoclaw.log`
|
||||
1. NanoClaw running: `launchctl list | grep nanoclaw` (macOS) / `systemctl --user status nanoclaw` (Linux)
|
||||
2. Messaging group wired: `sqlite3 data/v2.db "SELECT mg.platform_id, ag.folder FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id JOIN agent_groups ag ON ag.id = mga.agent_group_id WHERE mg.channel_type = 'emacs'"`
|
||||
3. Logs show inbound: `grep 'channel_type=emacs\|Emacs' logs/nanoclaw.log | tail -20`
|
||||
|
||||
If the group is not registered, it will be created automatically on the next NanoClaw restart.
|
||||
If no messaging group row exists, run the `register` command above.
|
||||
|
||||
### Auth token mismatch (401 Unauthorized)
|
||||
|
||||
Verify the token in Emacs matches `.env`:
|
||||
|
||||
```elisp
|
||||
;; M-x describe-variable RET nanoclaw-auth-token RET
|
||||
M-x describe-variable RET nanoclaw-auth-token RET
|
||||
```
|
||||
|
||||
Must exactly match `EMACS_AUTH_TOKEN` in `.env`.
|
||||
Must match `EMACS_AUTH_TOKEN` in `.env`. If you didn't set one server-side, clear it in Emacs too:
|
||||
|
||||
```elisp
|
||||
(setq nanoclaw-auth-token nil)
|
||||
```
|
||||
|
||||
### nanoclaw.el not loading
|
||||
|
||||
Check the path is correct:
|
||||
|
||||
```bash
|
||||
ls ~/src/nanoclaw/emacs/nanoclaw.el
|
||||
```
|
||||
|
||||
If NanoClaw is cloned elsewhere, update the `load`/`load-file` path in your Emacs config.
|
||||
|
||||
## 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 Formatting
|
||||
|
||||
The Emacs bridge converts markdown → org-mode automatically. Agents should
|
||||
output standard markdown — **not** org-mode syntax. The conversion handles:
|
||||
The Emacs bridge converts markdown → org-mode automatically. Agents should output standard markdown, **not** org-mode syntax:
|
||||
|
||||
| Markdown | Org-mode |
|
||||
|----------|----------|
|
||||
@@ -274,16 +278,19 @@ output standard markdown — **not** org-mode syntax. The conversion handles:
|
||||
| `` `code` `` | `~code~` |
|
||||
| ` ```lang ` | `#+begin_src lang` |
|
||||
|
||||
If an agent outputs org-mode directly, bold/italic/etc. will be double-converted
|
||||
and render incorrectly.
|
||||
If an agent outputs org-mode directly, markers get double-converted and render incorrectly.
|
||||
|
||||
## Removal
|
||||
|
||||
To remove the Emacs channel:
|
||||
```bash
|
||||
rm src/channels/emacs.ts src/channels/emacs.test.ts emacs/nanoclaw.el
|
||||
# Remove the `import './emacs.js';` line from src/channels/index.ts
|
||||
# Remove EMACS_* lines from .env
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# systemctl --user restart nanoclaw # Linux
|
||||
|
||||
1. Delete `src/channels/emacs.ts`, `src/channels/emacs.test.ts`, and `emacs/nanoclaw.el`
|
||||
2. Remove `import './emacs.js'` from `src/channels/index.ts`
|
||||
3. Remove the NanoClaw block from your Emacs config file
|
||||
4. Remove Emacs registration from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid = 'emacs:default'"`
|
||||
5. Remove `EMACS_CHANNEL_PORT` and `EMACS_AUTH_TOKEN` from `.env` if set
|
||||
6. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux)
|
||||
# Remove the NanoClaw block from your Emacs config
|
||||
# Optionally clean up the messaging group:
|
||||
sqlite3 data/v2.db "DELETE FROM messaging_group_agents WHERE messaging_group_id IN (SELECT id FROM messaging_groups WHERE channel_type='emacs'); DELETE FROM messaging_groups WHERE channel_type='emacs';"
|
||||
```
|
||||
|
||||
6
.claude/skills/add-gchat/REMOVE.md
Normal file
6
.claude/skills/add-gchat/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove Google Chat Channel
|
||||
|
||||
1. Comment out `import './gchat.js'` in `src/channels/index.ts`
|
||||
2. Remove `GCHAT_CREDENTIALS` from `.env`
|
||||
3. `pnpm uninstall @chat-adapter/gchat`
|
||||
4. Rebuild and restart
|
||||
92
.claude/skills/add-gchat/SKILL.md
Normal file
92
.claude/skills/add-gchat/SKILL.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: add-gchat
|
||||
description: Add Google Chat channel integration via Chat SDK.
|
||||
---
|
||||
|
||||
# Add Google Chat Channel
|
||||
|
||||
Adds Google Chat support via the Chat SDK bridge.
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Google Chat adapter in from the `channels` branch.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/gchat.ts` exists
|
||||
- `src/channels/index.ts` contains `import './gchat.js';`
|
||||
- `@chat-adapter/gchat` is listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/gchat.ts > src/channels/gchat.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './gchat.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/gchat@4.26.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
> 1. Go to [Google Cloud Console](https://console.cloud.google.com)
|
||||
> 2. Create or select a project
|
||||
> 3. Enable the **Google Chat API**
|
||||
> 4. Go to **Google Chat API** > **Configuration**:
|
||||
> - App name and description
|
||||
> - Connection settings: select **HTTP endpoint URL** and set to `https://your-domain/webhook/gchat`
|
||||
> 5. Create a **Service Account**:
|
||||
> - Go to **IAM & Admin** > **Service Accounts** > **Create Service Account**
|
||||
> - Grant the Chat Bot role
|
||||
> - Create a JSON key and download it
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add the service account JSON as a single-line string to `.env`:
|
||||
|
||||
```bash
|
||||
GCHAT_CREDENTIALS={"type":"service_account","project_id":"...","private_key":"...","client_email":"..."}
|
||||
```
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `gchat`
|
||||
- **terminology**: Google Chat has "spaces." A space can be a group conversation or a direct message with the bot.
|
||||
- **how-to-find-id**: Open the space in Google Chat, look at the URL — the space ID is the segment after `/space/` (e.g. `spaces/AAAA...`). Or use the Google Chat API to list spaces.
|
||||
- **supports-threads**: yes
|
||||
- **typical-use**: Interactive chat — team spaces or direct messages
|
||||
- **default-isolation**: Same agent group for spaces where you're the primary user. Separate agent group for spaces with different teams or sensitive contexts.
|
||||
3
.claude/skills/add-gchat/VERIFY.md
Normal file
3
.claude/skills/add-gchat/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify Google Chat Channel
|
||||
|
||||
Add the bot to a Google Chat space, then send a message or @mention the bot. The bot should respond within a few seconds.
|
||||
6
.claude/skills/add-github/REMOVE.md
Normal file
6
.claude/skills/add-github/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove GitHub Channel
|
||||
|
||||
1. Comment out `import './github.js'` in `src/channels/index.ts`
|
||||
2. Remove `GITHUB_TOKEN` and `GITHUB_WEBHOOK_SECRET` from `.env`
|
||||
3. `pnpm uninstall @chat-adapter/github`
|
||||
4. Rebuild and restart
|
||||
148
.claude/skills/add-github/SKILL.md
Normal file
148
.claude/skills/add-github/SKILL.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
name: add-github
|
||||
description: Add GitHub channel integration via Chat SDK. PR and issue comment threads as conversations.
|
||||
---
|
||||
|
||||
# Add GitHub Channel
|
||||
|
||||
Adds GitHub support via the Chat SDK bridge. The agent participates in PR and issue comment threads.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You need a **dedicated GitHub bot account** (not your personal account). The adapter uses this account to post replies and filters out its own messages to avoid loops. Create a free GitHub account for your bot (e.g. `my-org-bot`), then invite it as a collaborator with write access to the repos you want monitored.
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the GitHub adapter in from the `channels` branch.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/github.ts` exists
|
||||
- `src/channels/index.ts` contains `import './github.js';`
|
||||
- `@chat-adapter/github` is listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/github.ts > src/channels/github.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './github.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/github@4.26.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
### 1. Create a Personal Access Token for the bot account
|
||||
|
||||
Log in as your **bot account**, then:
|
||||
|
||||
1. Go to [Settings > Developer Settings > Personal Access Tokens](https://github.com/settings/tokens)
|
||||
2. Create a **Fine-grained token** with:
|
||||
- Repository access: select the repos you want the bot to monitor
|
||||
- Permissions: **Pull requests** (Read & Write), **Issues** (Read & Write)
|
||||
3. Copy the token
|
||||
|
||||
### 2. Set up a webhook on each repo
|
||||
|
||||
On each repo (logged in as the repo owner/admin):
|
||||
|
||||
1. Go to **Settings** > **Webhooks** > **Add webhook**
|
||||
2. Payload URL: `https://your-domain/webhook/github` (the shared webhook server, default port 3000)
|
||||
3. Content type: `application/json`
|
||||
4. Secret: generate a random string (e.g. `openssl rand -hex 20`)
|
||||
5. Events: select **Issue comments** and **Pull request review comments**
|
||||
|
||||
### 3. Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
GITHUB_TOKEN=github_pat_...
|
||||
GITHUB_WEBHOOK_SECRET=your-webhook-secret
|
||||
GITHUB_BOT_USERNAME=your-bot-username
|
||||
```
|
||||
|
||||
`GITHUB_BOT_USERNAME` must match the bot account's GitHub username exactly. This is used for @-mention detection — the agent responds when someone writes `@your-bot-username` in a PR or issue comment.
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
## Wiring
|
||||
|
||||
Ask the user: **Is this a private or public repo?**
|
||||
|
||||
- **Private repo** — use `unknown_sender_policy: 'public'`. Only collaborators can comment anyway, so it's safe to let all comments through.
|
||||
- **Public repo** — use `unknown_sender_policy: 'strict'`. Only registered members can trigger the agent, preventing strangers from consuming agent resources. Add trusted collaborators as members (see below).
|
||||
|
||||
Run `/manage-channels` to wire the GitHub channel to an agent group, or insert manually:
|
||||
|
||||
```sql
|
||||
-- Create messaging group (one per repo)
|
||||
INSERT INTO messaging_groups (id, channel_type, platform_id, name, is_group, unknown_sender_policy, created_at)
|
||||
VALUES ('mg-github-myrepo', 'github', 'github:owner/repo', 'owner/repo', 1, '<policy>', datetime('now'));
|
||||
|
||||
-- Wire to agent group
|
||||
INSERT INTO messaging_group_agents (id, messaging_group_id, agent_group_id, trigger_rules, response_scope, session_mode, priority, created_at)
|
||||
VALUES ('mga-github-myrepo', 'mg-github-myrepo', '<your-agent-group-id>', '', 'all', 'per-thread', 10, datetime('now'));
|
||||
```
|
||||
|
||||
Replace `<policy>` with `public` or `strict` based on the user's choice above.
|
||||
|
||||
### Adding members (for strict mode)
|
||||
|
||||
When using `strict`, add each GitHub user who should be able to trigger the agent:
|
||||
|
||||
```sql
|
||||
-- Add user (kind = 'github', id = 'github:<numeric-user-id>')
|
||||
INSERT OR IGNORE INTO users (id, kind, display_name, created_at)
|
||||
VALUES ('github:<user-id>', 'github', '<username>', datetime('now'));
|
||||
|
||||
-- Grant membership to the agent group
|
||||
INSERT OR IGNORE INTO agent_group_members (user_id, agent_group_id)
|
||||
VALUES ('github:<user-id>', '<agent-group-id>');
|
||||
```
|
||||
|
||||
To find a GitHub user's numeric ID: `gh api users/<username> --jq .id`
|
||||
|
||||
Use `per-thread` session mode so each PR/issue gets its own agent session.
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, restart the service (`systemctl --user restart nanoclaw` or `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`) to pick up the new channel.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `github`
|
||||
- **terminology**: GitHub has "repositories" containing "pull requests" and "issues." Each PR or issue comment thread is a separate conversation.
|
||||
- **how-to-find-id**: The platform ID is `github:owner/repo` (e.g. `github:acme/backend`). Each PR/issue becomes its own thread automatically.
|
||||
- **supports-threads**: yes (PR and issue comment threads are native conversations)
|
||||
- **typical-use**: Webhook-driven — the agent receives PR and issue comment events and responds in comment threads when @-mentioned. After the first mention, the thread is subscribed and the agent responds to all follow-up comments.
|
||||
- **default-isolation**: Use `per-thread` session mode. Each PR or issue gets its own isolated agent session. Typically wire to a dedicated agent group if the repo contains sensitive code.
|
||||
3
.claude/skills/add-github/VERIFY.md
Normal file
3
.claude/skills/add-github/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify GitHub Channel
|
||||
|
||||
@mention the bot in a PR comment or issue comment. The bot should respond within a few seconds.
|
||||
@@ -1,236 +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 || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
|
||||
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 with real tokens (not `onecli-managed` values), skip to "Build and restart" below.
|
||||
|
||||
### GCP Project Setup
|
||||
|
||||
Check if OneCLI is configured:
|
||||
|
||||
```bash
|
||||
grep -q 'ONECLI_URL=.' .env 2>/dev/null && echo "onecli" || echo "manual"
|
||||
```
|
||||
|
||||
**If OneCLI:** Tell the user to open `${ONECLI_URL}/connections?connect=gmail` to set up their Gmail connection. The dashboard walks them through creating a Google Cloud OAuth app and authorizing it. Ask them to let you know when done.
|
||||
|
||||
Once the user confirms, run:
|
||||
|
||||
```bash
|
||||
onecli apps get --provider gmail
|
||||
```
|
||||
|
||||
Check that `config.hasCredentials` is `true` or `connection` is not null. The response `hint` field has instructions and a docs URL for what stub credential files to create under `~/.gmail-mcp/`. Follow the hint — never overwrite existing files that don't contain `onecli-managed` values.
|
||||
|
||||
**If manual:** 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,94 +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 || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
6
.claude/skills/add-imessage/REMOVE.md
Normal file
6
.claude/skills/add-imessage/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove iMessage Channel
|
||||
|
||||
1. Comment out `import './imessage.js'` in `src/channels/index.ts`
|
||||
2. Remove iMessage env vars (`IMESSAGE_ENABLED`, `IMESSAGE_LOCAL`, `IMESSAGE_SERVER_URL`, `IMESSAGE_API_KEY`) from `.env`
|
||||
3. `pnpm uninstall chat-adapter-imessage`
|
||||
4. Rebuild and restart
|
||||
113
.claude/skills/add-imessage/SKILL.md
Normal file
113
.claude/skills/add-imessage/SKILL.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
name: add-imessage
|
||||
description: Add iMessage channel integration via Chat SDK. Local (macOS) or remote (Photon API) mode.
|
||||
---
|
||||
|
||||
# Add iMessage Channel
|
||||
|
||||
Adds iMessage support via the Chat SDK bridge. Two modes: local (macOS with Full Disk Access) or remote (Photon API).
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the iMessage adapter in from the `channels` branch.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/imessage.ts` exists
|
||||
- `src/channels/index.ts` contains `import './imessage.js';`
|
||||
- `chat-adapter-imessage` is listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/imessage.ts > src/channels/imessage.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './imessage.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install chat-adapter-imessage@0.1.1
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
### Local Mode (macOS)
|
||||
|
||||
Requirements: macOS with Full Disk Access granted to the Node.js binary.
|
||||
|
||||
The Node binary path is buried deep (e.g. `~/.nvm/versions/node/v22.x.x/bin/node`). To make it easy, open the folder in Finder so the user can drag the file into System Settings:
|
||||
|
||||
```bash
|
||||
open "$(dirname "$(which node)")"
|
||||
```
|
||||
|
||||
Then tell the user:
|
||||
|
||||
1. Open **System Settings** > **Privacy & Security** > **Full Disk Access**
|
||||
2. Click **+**, then drag the `node` file from the Finder window that just opened
|
||||
3. Toggle it on
|
||||
|
||||
Stop and wait for the user to confirm before continuing.
|
||||
|
||||
### Remote Mode (Photon API)
|
||||
|
||||
1. Set up a [Photon](https://photon.im) account
|
||||
2. Get your server URL and API key
|
||||
|
||||
### Configure environment
|
||||
|
||||
**Local mode** -- add to `.env`:
|
||||
|
||||
```bash
|
||||
IMESSAGE_ENABLED=true
|
||||
IMESSAGE_LOCAL=true
|
||||
```
|
||||
|
||||
**Remote mode** -- add to `.env`:
|
||||
|
||||
```bash
|
||||
IMESSAGE_LOCAL=false
|
||||
IMESSAGE_SERVER_URL=https://your-photon-server.com
|
||||
IMESSAGE_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `imessage`
|
||||
- **terminology**: iMessage has "conversations." Each conversation is with a contact identified by phone number or email address. Group chats are also supported.
|
||||
- **how-to-find-id**: The platform ID is the contact's phone number (e.g. `+15551234567`) or email address. For group chats, the ID is assigned by iMessage internally.
|
||||
- **supports-threads**: no
|
||||
- **typical-use**: Interactive 1:1 chat — personal messaging
|
||||
- **default-isolation**: Same agent group if you're the only person messaging the bot across iMessage and other channels. Separate agent group if different contacts should have information isolation.
|
||||
3
.claude/skills/add-imessage/VERIFY.md
Normal file
3
.claude/skills/add-imessage/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify iMessage Channel
|
||||
|
||||
Send an iMessage to the account running NanoClaw. The bot should respond within a few seconds.
|
||||
@@ -19,7 +19,7 @@ AskUserQuestion: "Which group should have the wiki?"
|
||||
2. **Dedicated group** — create a new group just for the wiki
|
||||
3. **Other** — pick an existing group
|
||||
|
||||
If dedicated: ask which channel and chat, then register with `npx tsx setup/index.ts --step register`.
|
||||
If dedicated: ask which channel and chat, then register with `pnpm exec tsx setup/index.ts --step register`.
|
||||
|
||||
## Step 3: Design collaboratively
|
||||
|
||||
@@ -74,7 +74,7 @@ AskUserQuestion: "Want periodic wiki health checks?"
|
||||
If yes, create a NanoClaw scheduled task that runs in the wiki group. This is NOT a Claude Code cron job — it's a NanoClaw group task that runs in the agent container. Insert it into the SQLite database:
|
||||
|
||||
```bash
|
||||
npx tsx -e "
|
||||
pnpm exec tsx -e "
|
||||
const Database = require('better-sqlite3');
|
||||
const { CronExpressionParser } = require('cron-parser');
|
||||
const db = new Database('store/messages.db');
|
||||
@@ -101,7 +101,7 @@ Use the group's `folder` and `chat_jid` from the registered groups table. Cron e
|
||||
## Step 6: Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
./container/build.sh
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
|
||||
6
.claude/skills/add-linear/REMOVE.md
Normal file
6
.claude/skills/add-linear/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove Linear Channel
|
||||
|
||||
1. Comment out `import './linear.js'` in `src/channels/index.ts`
|
||||
2. Remove `LINEAR_API_KEY` and `LINEAR_WEBHOOK_SECRET` from `.env`
|
||||
3. `pnpm uninstall @chat-adapter/linear`
|
||||
4. Rebuild and restart
|
||||
168
.claude/skills/add-linear/SKILL.md
Normal file
168
.claude/skills/add-linear/SKILL.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: add-linear
|
||||
description: Add Linear channel integration via Chat SDK. Issue comment threads as conversations.
|
||||
---
|
||||
|
||||
# Add Linear Channel
|
||||
|
||||
Adds Linear support via the Chat SDK bridge. The agent participates in issue comment threads. Every comment on a Linear issue triggers the agent — no @-mention needed.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**Recommended:** Create a Linear **OAuth application** so the agent posts as an app identity, not as you. This prevents the adapter from filtering your own comments as self-messages.
|
||||
|
||||
1. Go to [Linear Settings > API > OAuth Applications](https://linear.app/settings/api/applications/new)
|
||||
2. Create an app (e.g. "NanoClaw Bot")
|
||||
- Developer URL: your repo URL (e.g. `https://github.com/your-org/nanoclaw`)
|
||||
- Callback URL: `http://localhost`
|
||||
3. After creating, click the app and enable **Client credentials** under grant types
|
||||
4. Copy the **Client ID** and **Client Secret**
|
||||
|
||||
**Alternative:** Use a Personal API Key (`LINEAR_API_KEY`) for simpler setup. The agent will post as you, and your own comments will be filtered (other team members' comments still work).
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Linear adapter in from the `channels` branch and patches the Chat SDK bridge to support catch-all message forwarding (Linear OAuth apps can't be @-mentioned).
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/linear.ts` exists
|
||||
- `src/channels/index.ts` contains `import './linear.js';`
|
||||
- `@chat-adapter/linear` is listed in `package.json` dependencies
|
||||
- `src/channels/chat-sdk-bridge.ts` contains `catchAll`
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/linear.ts > src/channels/linear.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './linear.js';
|
||||
```
|
||||
|
||||
### 4. Patch the Chat SDK bridge for catch-all message forwarding
|
||||
|
||||
Linear OAuth apps can't be @-mentioned, so the bridge's `onNewMention` handler never fires. Add `catchAll` support to `src/channels/chat-sdk-bridge.ts`:
|
||||
|
||||
**4a.** Add `catchAll?: boolean` to the `ChatSdkBridgeConfig` interface:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Forward ALL messages in unsubscribed threads, not just @-mentions.
|
||||
* Use for platforms where the bot identity can't be @-mentioned (e.g.
|
||||
* Linear OAuth apps). The thread is auto-subscribed on first message.
|
||||
*/
|
||||
catchAll?: boolean;
|
||||
```
|
||||
|
||||
**4b.** Add this handler block right after the `chat.onNewMention(...)` block (before the DMs block):
|
||||
|
||||
```typescript
|
||||
// Catch-all for platforms where @-mention isn't possible (e.g. Linear
|
||||
// OAuth apps). Forward every unsubscribed message and auto-subscribe.
|
||||
if (config.catchAll) {
|
||||
chat.onNewMessage(/.*/, async (thread, message) => {
|
||||
const channelId = adapter.channelIdFromThreadId(thread.id);
|
||||
await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message));
|
||||
await thread.subscribe();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/linear@4.26.0
|
||||
```
|
||||
|
||||
### 6. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
### 1. Set up a webhook
|
||||
|
||||
1. Go to **Linear Settings** > **API** > **Webhooks** > **New webhook**
|
||||
2. Label: `NanoClaw`
|
||||
3. URL: `https://your-domain/webhook/linear` (the shared webhook server, default port 3000)
|
||||
4. Team: select the team you want to monitor
|
||||
5. Events: check **Comment**
|
||||
6. Save — copy the **signing secret**
|
||||
|
||||
Note: Linear webhook delivery may be delayed 1-5 minutes for new webhooks. This is normal.
|
||||
|
||||
### 2. Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
# OAuth app (recommended)
|
||||
LINEAR_CLIENT_ID=your-client-id
|
||||
LINEAR_CLIENT_SECRET=your-client-secret
|
||||
|
||||
# OR Personal API key (simpler, but agent posts as you)
|
||||
# LINEAR_API_KEY=lin_api_...
|
||||
|
||||
LINEAR_WEBHOOK_SECRET=your-webhook-signing-secret
|
||||
LINEAR_BOT_USERNAME=NanoClaw Bot
|
||||
LINEAR_TEAM_KEY=ENG
|
||||
```
|
||||
|
||||
- `LINEAR_BOT_USERNAME`: display name for the bot (used for self-message detection when using a Personal API Key)
|
||||
- `LINEAR_TEAM_KEY`: the Linear team key (e.g. `ENG`, `NAN`). Find it in Linear under Settings > Teams. All issues in this team route to one messaging group.
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
## Wiring
|
||||
|
||||
Ask the user: **Is this a private or public Linear workspace?**
|
||||
|
||||
- **Private workspace** — use `unknown_sender_policy: 'public'`. Only workspace members can comment.
|
||||
- **Public workspace** — use `unknown_sender_policy: 'strict'` and add trusted members (see GitHub skill for member registration example).
|
||||
|
||||
Run `/manage-channels` to wire the Linear channel to an agent group, or insert manually:
|
||||
|
||||
```sql
|
||||
-- Create messaging group (one per team)
|
||||
INSERT INTO messaging_groups (id, channel_type, platform_id, name, is_group, unknown_sender_policy, created_at)
|
||||
VALUES ('mg-linear-eng', 'linear', 'linear:ENG', 'Engineering', 1, 'public', datetime('now'));
|
||||
|
||||
-- Wire to agent group
|
||||
INSERT INTO messaging_group_agents (id, messaging_group_id, agent_group_id, trigger_rules, response_scope, session_mode, priority, created_at)
|
||||
VALUES ('mga-linear-eng', 'mg-linear-eng', '<your-agent-group-id>', '', 'all', 'per-thread', 10, datetime('now'));
|
||||
```
|
||||
|
||||
The `platform_id` must be `linear:<TEAM_KEY>` matching the `LINEAR_TEAM_KEY` env var. Use `per-thread` session mode so each issue comment thread gets its own agent session.
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, restart the service (`systemctl --user restart nanoclaw` or `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`) to pick up the new channel.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `linear`
|
||||
- **terminology**: Linear has "teams" containing "issues." Each issue's comment thread is a separate conversation.
|
||||
- **how-to-find-id**: The platform ID is `linear:<TEAM_KEY>` (e.g. `linear:ENG`). Find your team key in Linear under Settings > Teams. Each issue becomes its own thread automatically.
|
||||
- **supports-threads**: yes (issue comment threads are native conversations)
|
||||
- **typical-use**: Webhook-driven — the agent receives all issue comment events and responds automatically. No @-mention needed (Linear OAuth apps can't be @-mentioned).
|
||||
- **default-isolation**: Use `per-thread` session mode. Each issue comment thread gets its own isolated agent session.
|
||||
3
.claude/skills/add-linear/VERIFY.md
Normal file
3
.claude/skills/add-linear/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify Linear Channel
|
||||
|
||||
@mention the bot in a Linear issue comment. The bot should respond within a few seconds.
|
||||
6
.claude/skills/add-matrix/REMOVE.md
Normal file
6
.claude/skills/add-matrix/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove Matrix Channel
|
||||
|
||||
1. Comment out `import './matrix.js'` in `src/channels/index.ts`
|
||||
2. Remove `MATRIX_BASE_URL`, `MATRIX_ACCESS_TOKEN`, `MATRIX_USER_ID`, `MATRIX_BOT_USERNAME` from `.env`
|
||||
3. `pnpm uninstall @beeper/chat-adapter-matrix`
|
||||
4. Rebuild and restart
|
||||
148
.claude/skills/add-matrix/SKILL.md
Normal file
148
.claude/skills/add-matrix/SKILL.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
name: add-matrix
|
||||
description: Add Matrix channel integration via Chat SDK. Works with any Matrix homeserver.
|
||||
---
|
||||
|
||||
# Add Matrix Channel
|
||||
|
||||
Adds Matrix support via the Chat SDK bridge.
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Matrix adapter in from the `channels` branch.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/matrix.ts` exists
|
||||
- `src/channels/index.ts` contains `import './matrix.js';`
|
||||
- `@beeper/chat-adapter-matrix` is listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/matrix.ts > src/channels/matrix.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './matrix.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @beeper/chat-adapter-matrix@0.2.0
|
||||
```
|
||||
|
||||
### 5. Patch matrix-js-sdk ESM imports
|
||||
|
||||
The adapter's published dist references `matrix-js-sdk/lib/...` without `.js`
|
||||
extensions, which fails under Node 22 strict ESM resolution. Add the missing
|
||||
extensions (idempotent — safe to re-run):
|
||||
|
||||
```bash
|
||||
node -e '
|
||||
const fs = require("fs"), path = require("path");
|
||||
const root = "node_modules/.pnpm";
|
||||
const dir = fs.readdirSync(root).find(d => d.startsWith("@beeper+chat-adapter-matrix@"));
|
||||
if (!dir) { console.log("Matrix adapter not installed"); process.exit(0); }
|
||||
const f = path.join(root, dir, "node_modules/@beeper/chat-adapter-matrix/dist/index.js");
|
||||
fs.writeFileSync(f, fs.readFileSync(f, "utf8").replace(
|
||||
/from "(matrix-js-sdk\/lib\/[^"]+?)(?<!\.js)"/g, "from \"$1.js\""
|
||||
));
|
||||
console.log("Patched", f);
|
||||
'
|
||||
```
|
||||
|
||||
Re-run this after every `pnpm install` that touches the adapter.
|
||||
|
||||
### 6. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
The bot needs its own Matrix account — separate from the user's account. This is required because Matrix cannot send DMs to yourself.
|
||||
|
||||
### Create a bot account
|
||||
|
||||
1. Open [app.element.io](https://app.element.io) in a private/incognito window (or sign out first)
|
||||
2. Register a new account for the bot (e.g. `andybot` on matrix.org)
|
||||
3. Note the bot's user ID (e.g. `@andybot:matrix.org`)
|
||||
|
||||
### Choose an auth method
|
||||
|
||||
**Option A: Username + Password (simpler)**
|
||||
|
||||
No extra steps — just use the bot account's credentials directly. The adapter logs in automatically.
|
||||
|
||||
```bash
|
||||
MATRIX_BASE_URL=https://matrix.org
|
||||
MATRIX_USERNAME=andybot
|
||||
MATRIX_PASSWORD=your-bot-password
|
||||
MATRIX_USER_ID=@andybot:matrix.org
|
||||
MATRIX_BOT_USERNAME=Andy
|
||||
```
|
||||
|
||||
**Option B: Access Token (recommended for production)**
|
||||
|
||||
Get an access token from Element: sign into the bot account → **Settings** > **Help & About** > **Access Token** (under Advanced). Or via API:
|
||||
|
||||
```bash
|
||||
curl -XPOST 'https://matrix.org/_matrix/client/r0/login' \
|
||||
-d '{"type":"m.login.password","user":"andybot","password":"..."}'
|
||||
```
|
||||
|
||||
```bash
|
||||
MATRIX_BASE_URL=https://matrix.org
|
||||
MATRIX_ACCESS_TOKEN=your-access-token
|
||||
MATRIX_USER_ID=@andybot:matrix.org
|
||||
MATRIX_BOT_USERNAME=Andy
|
||||
```
|
||||
|
||||
### Optional settings
|
||||
|
||||
```bash
|
||||
MATRIX_INVITE_AUTOJOIN=true # Auto-accept room invites (default: true)
|
||||
MATRIX_INVITE_AUTOJOIN_ALLOWLIST=@you:matrix.org # Only accept invites from these users
|
||||
MATRIX_RECOVERY_KEY=your-recovery-key # Enable E2EE cross-signing
|
||||
MATRIX_DEVICE_ID=NANOCLAW01 # Stable device ID across restarts
|
||||
```
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add the chosen env vars to `.env`, then sync:
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `matrix`
|
||||
- **terminology**: Matrix has "rooms." A room can be a group chat or a direct message. Rooms have internal IDs (like `!abc123:matrix.org`) and optional aliases (like `#general:matrix.org`).
|
||||
- **how-to-find-id**: For DMs, use the bot's `openDM` to resolve the room automatically. For group rooms, in Element click the room name > Settings > Advanced — the "Internal room ID" is the platform ID (starts with `!`). Or use a room alias like `#general:matrix.org`.
|
||||
- **supports-threads**: partial (some clients support threads, but not all — treat as no for reliability)
|
||||
- **typical-use**: Interactive chat — rooms or direct messages. Requires a separate bot account (the agent cannot DM users from their own account).
|
||||
- **default-isolation**: Same agent group for rooms where you're the primary user. Separate agent group for rooms with different communities or sensitive contexts.
|
||||
3
.claude/skills/add-matrix/VERIFY.md
Normal file
3
.claude/skills/add-matrix/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify Matrix Channel
|
||||
|
||||
Invite the bot to a Matrix room and send a message. The bot should respond within a few seconds.
|
||||
179
.claude/skills/add-ollama-provider/SKILL.md
Normal file
179
.claude/skills/add-ollama-provider/SKILL.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
name: add-ollama-provider
|
||||
description: Route a NanoClaw agent group to a local Ollama model instead of the Anthropic API. Ollama speaks the Anthropic API natively (v1/messages), so no provider code changes are needed — just env var overrides and a model setting. Use when the user wants to run their agent locally, cut API costs, or experiment with open-weight models. See docs/ollama.md for background.
|
||||
---
|
||||
|
||||
# Add Ollama Provider
|
||||
|
||||
Routes an agent group to a local Ollama instance instead of the Anthropic API.
|
||||
See `docs/ollama.md` for how this works and the tradeoffs involved.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Ollama is installed and running** on the host — verify: `curl -s http://localhost:11434/api/tags`
|
||||
2. **A model is pulled** — e.g. `ollama pull gemma4` or `ollama pull qwen3-coder`
|
||||
3. **The agent group already exists** — run `/init-first-agent` first if needed
|
||||
|
||||
## 1. Check source support
|
||||
|
||||
The feature requires two fields in `ContainerConfig` (`env` and `blockedHosts`) and their
|
||||
corresponding wiring in `container-runner.ts`. Check if already present:
|
||||
|
||||
```bash
|
||||
grep -c 'blockedHosts' src/container-config.ts src/container-runner.ts
|
||||
```
|
||||
|
||||
If either count is 0, apply the changes in steps 1a and 1b. Otherwise skip to step 2.
|
||||
|
||||
### 1a. Extend ContainerConfig
|
||||
|
||||
In `src/container-config.ts`, add to the `ContainerConfig` interface:
|
||||
|
||||
```typescript
|
||||
env?: Record<string, string>;
|
||||
blockedHosts?: string[];
|
||||
```
|
||||
|
||||
And in `readContainerConfig`, add inside the returned object:
|
||||
|
||||
```typescript
|
||||
env: raw.env,
|
||||
blockedHosts: raw.blockedHosts,
|
||||
```
|
||||
|
||||
### 1b. Wire into container-runner
|
||||
|
||||
In `src/container-runner.ts`, after the `NANOCLAW_MCP_SERVERS` block, add:
|
||||
|
||||
```typescript
|
||||
// Per-agent-group env overrides — applied last to win over OneCLI values.
|
||||
if (containerConfig.env) {
|
||||
for (const [key, value] of Object.entries(containerConfig.env)) {
|
||||
args.push('-e', `${key}=${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Blocked hosts: resolve to 0.0.0.0 so they are unreachable inside the container.
|
||||
if (containerConfig.blockedHosts) {
|
||||
for (const host of containerConfig.blockedHosts) {
|
||||
args.push('--add-host', `${host}:0.0.0.0`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1c. Fix home directory permissions (if not already done)
|
||||
|
||||
The container may run as your host uid (not uid 1000). Check the Dockerfile:
|
||||
|
||||
```bash
|
||||
grep 'chmod.*home/node' container/Dockerfile
|
||||
```
|
||||
|
||||
If it shows `chmod 755`, change it to `chmod 777` so any uid can write there.
|
||||
Then rebuild the container image: `./container/build.sh`
|
||||
|
||||
## 2. Identify the setup
|
||||
|
||||
Ask the user (plain text, not AskUserQuestion):
|
||||
|
||||
1. **Which agent group?** List available groups: `sqlite3 data/v2.db "SELECT folder, name FROM agent_groups;"`
|
||||
2. **Which Ollama model?** List available: `curl -s http://localhost:11434/api/tags | grep '"name"'`
|
||||
3. **Block Anthropic API?** Recommended yes — prevents accidental spend if config drifts.
|
||||
|
||||
Record as `FOLDER`, `MODEL`, and `BLOCK_ANTHROPIC`.
|
||||
|
||||
## 3. Configure container.json
|
||||
|
||||
Read `groups/<FOLDER>/container.json`. Add (or merge into) an `env` block and optionally `blockedHosts`:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "http://host.docker.internal:11434",
|
||||
"ANTHROPIC_API_KEY": "ollama",
|
||||
"NO_PROXY": "host.docker.internal",
|
||||
"no_proxy": "host.docker.internal"
|
||||
},
|
||||
"blockedHosts": ["api.anthropic.com"]
|
||||
}
|
||||
```
|
||||
|
||||
Omit `blockedHosts` if the user declined step 2.
|
||||
|
||||
**Why these vars:** `ANTHROPIC_BASE_URL` redirects the Anthropic SDK to Ollama.
|
||||
`ANTHROPIC_API_KEY=ollama` satisfies the SDK's key requirement (Ollama ignores it).
|
||||
`NO_PROXY` bypasses the OneCLI HTTPS proxy for requests to `host.docker.internal`
|
||||
so they reach Ollama directly instead of going through the credential gateway.
|
||||
|
||||
## 4. Set the model
|
||||
|
||||
Read the agent group's shared Claude settings:
|
||||
|
||||
```bash
|
||||
# Find the agent group ID
|
||||
AG_ID=$(sqlite3 data/v2.db "SELECT id FROM agent_groups WHERE folder='<FOLDER>';")
|
||||
SETTINGS=data/v2-sessions/$AG_ID/.claude-shared/settings.json
|
||||
```
|
||||
|
||||
Add `"model": "<MODEL>"` to that settings file. Create the file if it doesn't exist:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gemma4:latest"
|
||||
}
|
||||
```
|
||||
|
||||
If the file already has content, merge the `model` key in — don't overwrite existing keys.
|
||||
|
||||
**Why here and not container.json:** Claude Code reads its model from its own settings
|
||||
file, not from env vars. This file is bind-mounted into the container as `~/.claude/settings.json`.
|
||||
|
||||
## 5. Build and restart
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
pnpm run build
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## 6. Verify
|
||||
|
||||
Send a message to the agent. Then confirm:
|
||||
|
||||
```bash
|
||||
# Ollama shows the model as active
|
||||
curl -s http://localhost:11434/api/ps | grep '"name"'
|
||||
|
||||
# Container has the right env vars
|
||||
CTR=$(docker ps --filter "name=nanoclaw-v2-<FOLDER>" --format "{{.Names}}" | head -1)
|
||||
docker inspect "$CTR" --format '{{json .HostConfig.ExtraHosts}}'
|
||||
docker exec "$CTR" env | grep ANTHROPIC
|
||||
```
|
||||
|
||||
Expected: `api.anthropic.com:0.0.0.0` in ExtraHosts, `ANTHROPIC_BASE_URL=http://host.docker.internal:11434`.
|
||||
|
||||
## Reverting to Claude
|
||||
|
||||
To switch back to the Anthropic API:
|
||||
|
||||
1. Remove the `env` and `blockedHosts` keys from `groups/<FOLDER>/container.json`
|
||||
2. Remove `"model"` from the shared settings file
|
||||
3. Restart the service
|
||||
|
||||
No rebuild needed — both files are read at container spawn time.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Agent hangs, no response:** Ollama may be loading the model cold (large models take 10–30s).
|
||||
Watch `curl -s http://localhost:11434/api/ps` — the model appears once loaded.
|
||||
|
||||
**"model not found" error in container logs:** The model name in settings.json doesn't match
|
||||
what Ollama has. Run `ollama list` on the host and use the exact name shown.
|
||||
|
||||
**Responses claim to be Claude:** The model was trained on data that includes Claude conversations.
|
||||
Add a line to `groups/<FOLDER>/CLAUDE.md` telling it what model it runs on.
|
||||
|
||||
**Agent responds but Ollama shows no activity:** `NO_PROXY` may not have taken effect for
|
||||
`http_proxy` (lowercase). Add both `NO_PROXY` and `no_proxy` to the env block.
|
||||
@@ -87,7 +87,7 @@ done
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
./container/build.sh
|
||||
```
|
||||
|
||||
|
||||
229
.claude/skills/add-opencode/SKILL.md
Normal file
229
.claude/skills/add-opencode/SKILL.md
Normal file
@@ -0,0 +1,229 @@
|
||||
---
|
||||
name: add-opencode
|
||||
description: Use OpenCode as an agent provider (AGENT_PROVIDER=opencode). OpenRouter, OpenAI, Google, DeepSeek, etc. via OpenCode config — not the Anthropic Agent SDK. Per-session and per-group via agent_provider; host passes OPENCODE_* and XDG mount when spawning containers.
|
||||
---
|
||||
|
||||
# OpenCode agent provider
|
||||
|
||||
NanoClaw runs agents in a long-lived **poll loop** inside the container. The backend is selected with **`AGENT_PROVIDER`** (`claude` | `opencode` | `mock`).
|
||||
|
||||
Trunk ships with only the `claude` provider baked in. This skill copies the OpenCode provider files in from the `providers` branch, wires them into the host and container barrels, installs dependencies, and rebuilds the image.
|
||||
|
||||
## Install
|
||||
|
||||
### Pre-flight
|
||||
|
||||
If all of the following are already present, skip to **Configuration**:
|
||||
|
||||
- `src/providers/opencode.ts`
|
||||
- `container/agent-runner/src/providers/opencode.ts`
|
||||
- `import './opencode.js';` line in `src/providers/index.ts`
|
||||
- `import './opencode.js';` line in `container/agent-runner/src/providers/index.ts`
|
||||
- `@opencode-ai/sdk` in `container/agent-runner/package.json`
|
||||
- `opencode-ai@${OPENCODE_VERSION}` in the pnpm global-install block in `container/Dockerfile`
|
||||
|
||||
Missing pieces — continue below. All steps are idempotent; re-running is safe.
|
||||
|
||||
### 1. Fetch the providers branch
|
||||
|
||||
```bash
|
||||
git fetch origin providers
|
||||
```
|
||||
|
||||
### 2. Copy the OpenCode source files
|
||||
|
||||
Wholesale copies (owned entirely by this skill — user edits to these files won't survive a re-run, as designed):
|
||||
|
||||
```bash
|
||||
git show origin/providers:src/providers/opencode.ts > src/providers/opencode.ts
|
||||
git show origin/providers:container/agent-runner/src/providers/opencode.ts > container/agent-runner/src/providers/opencode.ts
|
||||
git show origin/providers:container/agent-runner/src/providers/mcp-to-opencode.ts > container/agent-runner/src/providers/mcp-to-opencode.ts
|
||||
git show origin/providers:container/agent-runner/src/providers/mcp-to-opencode.test.ts > container/agent-runner/src/providers/mcp-to-opencode.test.ts
|
||||
git show origin/providers:container/agent-runner/src/providers/opencode.factory.test.ts > container/agent-runner/src/providers/opencode.factory.test.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration imports
|
||||
|
||||
Each barrel gets one line appended at the end — skip if the line is already present.
|
||||
|
||||
`src/providers/index.ts`:
|
||||
|
||||
```typescript
|
||||
import './opencode.js';
|
||||
```
|
||||
|
||||
`container/agent-runner/src/providers/index.ts`:
|
||||
|
||||
```typescript
|
||||
import './opencode.js';
|
||||
```
|
||||
|
||||
### 4. Add the agent-runner dependency
|
||||
|
||||
Pinned. Bump deliberately, not with `bun update`. Use `1.4.17` — must match the `opencode-ai` CLI version pinned in step 5. The 1.14.x SDK has a completely different API and is **incompatible** with the current provider code.
|
||||
|
||||
```bash
|
||||
cd container/agent-runner && bun add @opencode-ai/sdk@1.4.17 && cd -
|
||||
```
|
||||
|
||||
### 5. Add `opencode-ai` to the container Dockerfile
|
||||
|
||||
Two edits to `container/Dockerfile`, both idempotent (skip if already present):
|
||||
|
||||
**(a)** In the "Pin CLI versions" ARG block (around line 18), add after `ARG VERCEL_VERSION=latest`:
|
||||
|
||||
```dockerfile
|
||||
ARG OPENCODE_VERSION=1.4.17
|
||||
```
|
||||
|
||||
> **Do not use `latest`** — the CLI and SDK must be the same version. `latest` silently upgrades the CLI to 1.14.x which has a breaking session API change (UUID session IDs → `ses_` prefix) incompatible with SDK 1.4.x.
|
||||
|
||||
**(b)** In the `pnpm install -g` block (around line 80), append `"opencode-ai@${OPENCODE_VERSION}"` to the list:
|
||||
|
||||
```dockerfile
|
||||
pnpm install -g \
|
||||
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
|
||||
"agent-browser@${AGENT_BROWSER_VERSION}" \
|
||||
"vercel@${VERCEL_VERSION}" \
|
||||
"opencode-ai@${OPENCODE_VERSION}"
|
||||
```
|
||||
|
||||
### 6. Build
|
||||
|
||||
```bash
|
||||
pnpm run build # host
|
||||
pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit # container typecheck
|
||||
./container/build.sh # agent image
|
||||
```
|
||||
|
||||
> **Build cache gotcha:** The container buildkit caches COPY steps aggressively. If provider files were already present in the build context before, the new files may not be picked up. If you see "Unknown provider: opencode" after the build, prune the builder and rebuild:
|
||||
> ```bash
|
||||
> docker builder prune -f && ./container/build.sh
|
||||
> ```
|
||||
|
||||
### 7. Propagate to existing per-group overlays
|
||||
|
||||
Each agent group has a live source overlay at `data/v2-sessions/<group-id>/agent-runner-src/providers/` that **overrides the image at runtime**. This overlay is created when the group is first wired and never auto-updated by image rebuilds. Any group that already existed before this skill ran needs the new files copied in manually.
|
||||
|
||||
```bash
|
||||
for overlay in data/v2-sessions/*/agent-runner-src/providers/; do
|
||||
[ -d "$overlay" ] || continue
|
||||
cp container/agent-runner/src/providers/opencode.ts "$overlay"
|
||||
cp container/agent-runner/src/providers/mcp-to-opencode.ts "$overlay"
|
||||
cp container/agent-runner/src/providers/index.ts "$overlay"
|
||||
echo "Updated: $overlay"
|
||||
done
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Host `.env` (typical)
|
||||
|
||||
Set model/provider strings in the form OpenCode expects (often `provider/model-id`). **Put comments on their own lines** — a `#` inside a value is kept verbatim and breaks model IDs.
|
||||
|
||||
These variables are read **on the host** and passed into the container only when the effective provider is `opencode`. They do not switch the provider by themselves; the DB still needs `agent_provider` set (below).
|
||||
|
||||
- `OPENCODE_PROVIDER` — OpenCode provider id, e.g. `openrouter`, `anthropic`, `deepseek`.
|
||||
- `OPENCODE_MODEL` — full model id in `provider/model` form, e.g. `deepseek/deepseek-chat`.
|
||||
- `OPENCODE_SMALL_MODEL` — optional second model for lighter tasks; defaults to `OPENCODE_MODEL` if unset.
|
||||
- `ANTHROPIC_BASE_URL` — **required for non-`anthropic` providers.** The opencode container provider passes this as the `baseURL` for the upstream provider config so requests route through OneCLI's credential proxy or directly to the provider's API. Set it to the provider's API base URL (e.g. `https://api.deepseek.com/v1`, `https://openrouter.ai/api/v1`).
|
||||
|
||||
Credentials: register provider API keys in OneCLI with the matching `--host-pattern` (e.g. `api.deepseek.com`, `openrouter.ai`). OneCLI injects them via `HTTPS_PROXY` in the container — the key never lives in `.env` or the container environment.
|
||||
|
||||
After adding a secret, **grant the agent access** — agents in `selective` mode only receive secrets they've been explicitly assigned:
|
||||
|
||||
```bash
|
||||
# Find the agent id and secret id, then:
|
||||
onecli agents set-secrets --id <agent-id> --secret-ids <existing-ids>,<new-secret-id>
|
||||
```
|
||||
|
||||
Always include existing secret IDs in the list — `set-secrets` replaces, not appends.
|
||||
|
||||
#### Example: DeepSeek
|
||||
|
||||
```env
|
||||
OPENCODE_PROVIDER=deepseek
|
||||
OPENCODE_MODEL=deepseek/deepseek-chat
|
||||
OPENCODE_SMALL_MODEL=deepseek/deepseek-chat
|
||||
ANTHROPIC_BASE_URL=https://api.deepseek.com/v1
|
||||
```
|
||||
|
||||
Register the key:
|
||||
```bash
|
||||
onecli secrets create --name "DeepSeek" --type generic \
|
||||
--value YOUR_KEY --host-pattern "api.deepseek.com" \
|
||||
--header-name "Authorization" --value-format "Bearer {value}"
|
||||
```
|
||||
|
||||
#### Example: OpenRouter
|
||||
|
||||
```env
|
||||
OPENCODE_PROVIDER=openrouter
|
||||
OPENCODE_MODEL=openrouter/anthropic/claude-sonnet-4
|
||||
OPENCODE_SMALL_MODEL=openrouter/anthropic/claude-haiku-4.5
|
||||
ANTHROPIC_BASE_URL=https://openrouter.ai/api/v1
|
||||
```
|
||||
|
||||
Register the key:
|
||||
```bash
|
||||
onecli secrets create --name "OpenRouter" --type generic \
|
||||
--value YOUR_KEY --host-pattern "openrouter.ai" \
|
||||
--header-name "Authorization" --value-format "Bearer {value}"
|
||||
```
|
||||
|
||||
#### Example: Anthropic (no ANTHROPIC_BASE_URL needed)
|
||||
|
||||
When `OPENCODE_PROVIDER` is `anthropic`, OpenCode uses normal Anthropic env inside the container — the proxy + placeholder key pattern is unchanged and `ANTHROPIC_BASE_URL` is not required.
|
||||
|
||||
```env
|
||||
OPENCODE_PROVIDER=anthropic
|
||||
OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514
|
||||
OPENCODE_SMALL_MODEL=anthropic/claude-haiku-4-5-20251001
|
||||
```
|
||||
|
||||
#### OpenCode Zen (`x-api-key`, not Bearer)
|
||||
|
||||
Zen's HTTP API (e.g. `POST …/zen/v1/messages`) expects the key in the **`x-api-key`** header. If OneCLI injects **`Authorization: Bearer …`** only, Zen often returns **401 / "Missing API key"** even though the gateway is working.
|
||||
|
||||
**Naming:** NanoClaw **`AGENT_PROVIDER=opencode`** (DB `agent_provider`) means "run the **OpenCode agent provider**." Separately, **`OPENCODE_PROVIDER=opencode`** in `.env` is OpenCode's **Zen provider id** inside the OpenCode config (see [Zen docs](https://opencode.ai/docs/zen/)).
|
||||
|
||||
**Host `.env` (typical Zen shape):**
|
||||
|
||||
```env
|
||||
OPENCODE_PROVIDER=opencode
|
||||
OPENCODE_MODEL=opencode/big-pickle
|
||||
OPENCODE_SMALL_MODEL=opencode/big-pickle
|
||||
ANTHROPIC_BASE_URL=https://opencode.ai/zen/v1
|
||||
```
|
||||
|
||||
Use a real Zen model id from the docs; `big-pickle` is one example.
|
||||
|
||||
**OneCLI:** register the Zen key with **`x-api-key`**, not Bearer:
|
||||
|
||||
```bash
|
||||
onecli secrets create --name "OpenCode Zen" --type generic \
|
||||
--value YOUR_ZEN_KEY --host-pattern opencode.ai \
|
||||
--header-name "x-api-key" --value-format "{value}"
|
||||
```
|
||||
|
||||
### Per group / per session
|
||||
|
||||
Schema: **`agent_groups.agent_provider`** and **`sessions.agent_provider`**. Set to `opencode` for groups or sessions that should use OpenCode. The container receives `AGENT_PROVIDER` from the resolved value (session overrides group).
|
||||
|
||||
Extra MCP servers still come from **`NANOCLAW_MCP_SERVERS`** / `container_config.mcpServers` on the host; the runner merges them into the same `mcpServers` object passed to **both** Claude and OpenCode providers.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- OpenCode keeps a local **`opencode serve`** process and SSE subscription; the provider tears down with **`stream.return`** and **SIGKILL** on the server process on **`abort()`** / shared runtime reset to avoid MCP/zombie hangs.
|
||||
- Session continuation uses UUID format (SDK 1.4.x / CLI 1.4.x). Stale sessions are cleared by `isSessionInvalid` on OpenCode-specific error patterns. If you see UUID-related errors after an accidental CLI upgrade, clear `session_state` in `outbound.db` and wipe the `opencode-xdg` directory under the session folder.
|
||||
- **`NO_PROXY`** for localhost matters when the OpenCode client talks to `127.0.0.1` inside the container while HTTP(S)_PROXY is set (e.g. OneCLI).
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
grep -q "./opencode.js" container/agent-runner/src/providers/index.ts && echo "container barrel: OK"
|
||||
grep -q "./opencode.js" src/providers/index.ts && echo "host barrel: OK"
|
||||
grep -q "@opencode-ai/sdk" container/agent-runner/package.json && echo "agent-runner dep: OK"
|
||||
grep -q "opencode-ai@" container/Dockerfile && echo "Dockerfile install: OK"
|
||||
cd container/agent-runner && bun test src/providers/ && cd -
|
||||
```
|
||||
@@ -232,7 +232,7 @@ echo '{}' | docker run -i --entrypoint /bin/echo nanoclaw-agent:latest "Containe
|
||||
Rebuild the main app and restart:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
@@ -286,5 +286,5 @@ 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`
|
||||
4. Rebuild: `./container/build.sh && pnpm run build`
|
||||
5. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
|
||||
|
||||
@@ -1,104 +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 || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
|
||||
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,117 +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 || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
6
.claude/skills/add-resend/REMOVE.md
Normal file
6
.claude/skills/add-resend/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove Resend Email Channel
|
||||
|
||||
1. Comment out `import './resend.js'` in `src/channels/index.ts`
|
||||
2. Remove `RESEND_API_KEY`, `RESEND_FROM_ADDRESS`, `RESEND_FROM_NAME`, `RESEND_WEBHOOK_SECRET` from `.env`
|
||||
3. `pnpm uninstall @resend/chat-sdk-adapter`
|
||||
4. Rebuild and restart
|
||||
93
.claude/skills/add-resend/SKILL.md
Normal file
93
.claude/skills/add-resend/SKILL.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: add-resend
|
||||
description: Add Resend (email) channel integration via Chat SDK.
|
||||
---
|
||||
|
||||
# Add Resend Email Channel
|
||||
|
||||
Connect NanoClaw to email via Resend for async email conversations.
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Resend adapter in from the `channels` branch.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/resend.ts` exists
|
||||
- `src/channels/index.ts` contains `import './resend.js';`
|
||||
- `@resend/chat-sdk-adapter` is listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/resend.ts > src/channels/resend.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './resend.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @resend/chat-sdk-adapter@0.1.1
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
1. Go to [resend.com](https://resend.com) and create an account.
|
||||
2. Add and verify your sending domain.
|
||||
3. Go to **API Keys** and create a new key.
|
||||
4. Set up a webhook:
|
||||
- Go to **Webhooks** > **Add webhook**.
|
||||
- URL: `https://your-domain/webhook/resend`.
|
||||
- Events: select **email.received**.
|
||||
- Copy the signing secret.
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
RESEND_API_KEY=re_...
|
||||
RESEND_FROM_ADDRESS=bot@yourdomain.com
|
||||
RESEND_FROM_NAME=NanoClaw
|
||||
RESEND_WEBHOOK_SECRET=your-webhook-secret
|
||||
```
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `resend`
|
||||
- **terminology**: Resend handles email. Each email thread (identified by subject/In-Reply-To headers) is a separate conversation. The "from address" is the bot's identity.
|
||||
- **how-to-find-id**: The platform ID is the from email address (e.g. `bot@yourdomain.com`). Each sender's email thread becomes its own conversation.
|
||||
- **supports-threads**: yes (via email threading headers -- replies to the same thread stay together)
|
||||
- **typical-use**: Async communication -- email conversations with longer response expectations
|
||||
- **default-isolation**: Same agent group if you want your agent to handle email alongside other channels. Separate agent group if email contains sensitive correspondence that shouldn't be accessible from other channels.
|
||||
3
.claude/skills/add-resend/VERIFY.md
Normal file
3
.claude/skills/add-resend/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify Resend Email Channel
|
||||
|
||||
Send an email to the configured from address. The bot should respond via email within a few seconds.
|
||||
6
.claude/skills/add-slack/REMOVE.md
Normal file
6
.claude/skills/add-slack/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove Slack
|
||||
|
||||
1. Comment out `import './slack.js'` in `src/channels/index.ts`
|
||||
2. Remove `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` from `.env`
|
||||
3. `pnpm uninstall @chat-adapter/slack`
|
||||
4. Rebuild and restart
|
||||
@@ -1,80 +1,82 @@
|
||||
---
|
||||
name: add-slack
|
||||
description: Add Slack as a channel. Can replace WhatsApp entirely or run alongside it. Uses Socket Mode (no public URL needed).
|
||||
description: Add Slack channel integration via Chat SDK.
|
||||
---
|
||||
|
||||
# Add Slack Channel
|
||||
|
||||
This skill adds Slack support to NanoClaw, then walks through interactive setup.
|
||||
Adds Slack support via the Chat SDK bridge.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
## Install
|
||||
|
||||
### Check if already applied
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Slack adapter in from the `channels` branch.
|
||||
|
||||
Check if `src/channels/slack.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
### Ask the user
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
**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.
|
||||
- `src/channels/slack.ts` exists
|
||||
- `src/channels/index.ts` contains `import './slack.js';`
|
||||
- `@chat-adapter/slack` is listed in `package.json` dependencies
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### Ensure channel remote
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
If `slack` is missing, add it:
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git remote add slack https://github.com/qwibitai/nanoclaw-slack.git
|
||||
git show origin/channels:src/channels/slack.ts > src/channels/slack.ts
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './slack.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
git fetch slack main
|
||||
git merge slack/main || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
pnpm install @chat-adapter/slack@4.26.0
|
||||
```
|
||||
|
||||
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
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/channels/slack.test.ts
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
All tests must pass (including the new Slack tests) and build must be clean before proceeding.
|
||||
## Credentials
|
||||
|
||||
## Phase 3: Setup
|
||||
### Create Slack App
|
||||
|
||||
### Create Slack App (if needed)
|
||||
1. Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App** > **From scratch**
|
||||
2. Name it (e.g., "NanoClaw") and select your workspace
|
||||
3. Go to **OAuth & Permissions** and add Bot Token Scopes:
|
||||
- `chat:write`, `channels:history`, `groups:history`, `im:history`, `channels:read`, `groups:read`, `users:read`, `reactions:write`
|
||||
4. Click **Install to Workspace** and copy the **Bot User OAuth Token** (`xoxb-...`)
|
||||
5. Go to **Basic Information** and copy the **Signing Secret**
|
||||
|
||||
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.
|
||||
### Enable DMs
|
||||
|
||||
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-...`)
|
||||
6. Go to **App Home** and enable the **Messages Tab**
|
||||
7. Check **"Allow users to send Slash commands and messages from the messages tab"**
|
||||
|
||||
Wait for the user to provide both tokens.
|
||||
### Event Subscriptions
|
||||
|
||||
8. Go to **Event Subscriptions** and toggle **Enable Events**
|
||||
9. Set the **Request URL** to `https://your-domain/webhook/slack` — Slack will send a verification challenge; it must pass before you can save
|
||||
10. Under **Subscribe to bot events**, add:
|
||||
- `message.channels`, `message.groups`, `message.im`, `app_mention`
|
||||
11. Click **Save Changes**
|
||||
12. Slack will show a banner asking you to **reinstall the app** — click it to apply the new event subscriptions
|
||||
|
||||
### Configure environment
|
||||
|
||||
@@ -82,126 +84,29 @@ Add to `.env`:
|
||||
|
||||
```bash
|
||||
SLACK_BOT_TOKEN=xoxb-your-bot-token
|
||||
SLACK_APP_TOKEN=xapp-your-app-token
|
||||
SLACK_SIGNING_SECRET=your-signing-secret
|
||||
```
|
||||
|
||||
Channels auto-enable when their credentials are present — no extra configuration needed.
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
Sync to container environment:
|
||||
### Webhook server
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
```
|
||||
The Chat SDK bridge automatically starts a shared webhook server on port 3000 (configurable via `WEBHOOK_PORT` env var). The server handles `/webhook/slack` for Slack and other webhook-based adapters. This port must be publicly reachable from the internet for Slack to deliver events.
|
||||
|
||||
The container reads environment from `data/env/env`, not `.env` directly.
|
||||
If running locally, discuss options for exposing the server — e.g. ngrok (`ngrok http 3000`), Cloudflare Tunnel, or a reverse proxy on a VPS. The resulting public URL becomes the base for `https://your-domain/webhook/slack`.
|
||||
|
||||
### Build and restart
|
||||
## Next Steps
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
```
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
## Phase 4: Registration
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
### Get Channel ID
|
||||
## Channel Info
|
||||
|
||||
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
|
||||
|
||||
The channel ID, name, and folder name are needed. Use `npx tsx setup/index.ts --step register` with the appropriate flags.
|
||||
|
||||
For a main channel (responds to all messages):
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step register -- --jid "slack:<channel-id>" --name "<channel-name>" --folder "slack_main" --trigger "@${ASSISTANT_NAME}" --channel slack --no-trigger-required --is-main
|
||||
```
|
||||
|
||||
For additional channels (trigger-only):
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step register -- --jid "slack:<channel-id>" --name "<channel-name>" --folder "slack_<channel-name>" --trigger "@${ASSISTANT_NAME}" --channel slack
|
||||
```
|
||||
|
||||
## 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.
|
||||
- **type**: `slack`
|
||||
- **terminology**: Slack has "workspaces" containing "channels." Channels can be public (#general) or private. The bot can also receive direct messages.
|
||||
- **platform-id-format**: `slack:{channelId}` for channels (e.g., `slack:C0123ABC`), `slack:{dmId}` for DMs (e.g., `slack:D0ARWEBLV63`)
|
||||
- **how-to-find-id**: Right-click a channel name > "View channel details" — the Channel ID is at the bottom (starts with C). For DMs, the ID starts with D. Or copy the channel link — the ID is the last segment of the URL.
|
||||
- **supports-threads**: yes
|
||||
- **typical-use**: Interactive chat — team channels or direct messages
|
||||
- **default-isolation**: Same agent group for channels where you're the primary user. Separate agent group for channels with different teams or sensitive contexts.
|
||||
|
||||
3
.claude/skills/add-slack/VERIFY.md
Normal file
3
.claude/skills/add-slack/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify Slack
|
||||
|
||||
Add the bot to a Slack channel, then send a message or @mention the bot. The bot should respond within a few seconds.
|
||||
6
.claude/skills/add-teams/REMOVE.md
Normal file
6
.claude/skills/add-teams/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove Microsoft Teams Channel
|
||||
|
||||
1. Comment out `import './teams.js'` in `src/channels/index.ts`
|
||||
2. Remove `TEAMS_APP_ID` and `TEAMS_APP_PASSWORD` from `.env`
|
||||
3. `pnpm uninstall @chat-adapter/teams`
|
||||
4. Rebuild and restart
|
||||
207
.claude/skills/add-teams/SKILL.md
Normal file
207
.claude/skills/add-teams/SKILL.md
Normal file
@@ -0,0 +1,207 @@
|
||||
---
|
||||
name: add-teams
|
||||
description: Add Microsoft Teams channel integration via Chat SDK.
|
||||
---
|
||||
|
||||
# Add Microsoft Teams Channel
|
||||
|
||||
Connect NanoClaw to Microsoft Teams for interactive chat in team channels, group chats, and direct messages.
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Teams adapter in from the `channels` branch.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/teams.ts` exists
|
||||
- `src/channels/index.ts` contains `import './teams.js';`
|
||||
- `@chat-adapter/teams` is listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/teams.ts > src/channels/teams.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './teams.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/teams@4.26.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
### Step 1: Create an Azure AD App Registration
|
||||
|
||||
1. Go to [Azure Portal](https://portal.azure.com) > **App registrations** > **New registration**
|
||||
2. Name it (e.g., "NanoClaw")
|
||||
3. Supported account types: **Single tenant** (your org only) or **Multi tenant** (any org)
|
||||
4. Click **Register**
|
||||
5. Copy the **Application (client) ID** and **Directory (tenant) ID** from the Overview page
|
||||
|
||||
### Step 2: Create a Client Secret
|
||||
|
||||
1. In the App Registration, go to **Certificates & secrets**
|
||||
2. Click **New client secret**, description "nanoclaw", expiry 180 days
|
||||
3. Click **Add** and **copy the Value immediately** (shown only once)
|
||||
|
||||
### Step 3: Create an Azure Bot
|
||||
|
||||
1. Go to Azure Portal > search **Azure Bot** > **Create**
|
||||
2. Fill in:
|
||||
- **Bot handle**: unique name (e.g., "nanoclaw-bot")
|
||||
- **Type of App**: match your app registration (Single or Multi Tenant)
|
||||
- **Creation type**: **Use existing app registration**
|
||||
- **App ID**: paste from Step 1
|
||||
- **App tenant ID**: paste from Step 1 (Single Tenant only)
|
||||
3. Click **Review + create** > **Create**
|
||||
|
||||
Or use Azure CLI:
|
||||
|
||||
```bash
|
||||
az group create --name nanoclaw-rg --location eastus
|
||||
az bot create \
|
||||
--resource-group nanoclaw-rg \
|
||||
--name nanoclaw-bot \
|
||||
--app-type SingleTenant \
|
||||
--appid YOUR_APP_ID \
|
||||
--tenant-id YOUR_TENANT_ID \
|
||||
--endpoint "https://your-domain/api/webhooks/teams"
|
||||
```
|
||||
|
||||
### Step 4: Configure Messaging Endpoint
|
||||
|
||||
1. Go to your Azure Bot resource > **Configuration**
|
||||
2. Set **Messaging endpoint** to `https://your-domain/api/webhooks/teams`
|
||||
3. Click **Apply**
|
||||
|
||||
### Step 5: Enable Teams Channel
|
||||
|
||||
1. In the Azure Bot resource, go to **Channels**
|
||||
2. Click **Microsoft Teams** > Accept terms > **Apply**
|
||||
|
||||
Or via CLI:
|
||||
|
||||
```bash
|
||||
az bot msteams create --resource-group nanoclaw-rg --name nanoclaw-bot
|
||||
```
|
||||
|
||||
### Step 6: Create and Sideload Teams App
|
||||
|
||||
Create a `manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
|
||||
"manifestVersion": "1.16",
|
||||
"version": "1.0.0",
|
||||
"id": "YOUR_APP_ID",
|
||||
"packageName": "com.nanoclaw.bot",
|
||||
"developer": {
|
||||
"name": "NanoClaw",
|
||||
"websiteUrl": "https://your-domain",
|
||||
"privacyUrl": "https://your-domain",
|
||||
"termsOfUseUrl": "https://your-domain"
|
||||
},
|
||||
"name": { "short": "NanoClaw", "full": "NanoClaw Assistant" },
|
||||
"description": {
|
||||
"short": "NanoClaw assistant bot",
|
||||
"full": "NanoClaw personal assistant powered by Claude."
|
||||
},
|
||||
"icons": { "outline": "outline.png", "color": "color.png" },
|
||||
"accentColor": "#4A90D9",
|
||||
"bots": [{
|
||||
"botId": "YOUR_APP_ID",
|
||||
"scopes": ["personal", "team", "groupchat"],
|
||||
"supportsFiles": false,
|
||||
"isNotificationOnly": false
|
||||
}],
|
||||
"permissions": ["identity", "messageTeamMembers"],
|
||||
"validDomains": ["your-domain"]
|
||||
}
|
||||
```
|
||||
|
||||
Create two icon PNGs (32x32 `outline.png`, 192x192 `color.png`), zip all three files together.
|
||||
|
||||
**Sideload in Teams:**
|
||||
1. Open Teams > **Apps** > **Manage your apps**
|
||||
2. Click **Upload an app** > **Upload a custom app**
|
||||
3. Select the zip file
|
||||
|
||||
Sideloading requires Teams admin access. Free personal Teams does NOT support sideloading. Use a Microsoft 365 Business account or developer tenant.
|
||||
|
||||
### Step 7: Receive All Messages (Optional)
|
||||
|
||||
By default, the bot only receives messages when @-mentioned. To receive all messages in a channel without @-mention, add RSC permissions to `manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"authorization": {
|
||||
"permissions": {
|
||||
"resourceSpecific": [
|
||||
{ "name": "ChannelMessage.Read.Group", "type": "Application" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
TEAMS_APP_ID=your-app-id
|
||||
TEAMS_APP_PASSWORD=your-client-secret
|
||||
# For Single Tenant only:
|
||||
TEAMS_APP_TENANT_ID=your-tenant-id
|
||||
TEAMS_APP_TYPE=SingleTenant
|
||||
```
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
### Webhook server
|
||||
|
||||
The Chat SDK bridge automatically starts a shared webhook server on port 3000 (configurable via `WEBHOOK_PORT` env var). The server handles `/api/webhooks/teams` for Teams and other webhook-based adapters. This port must be publicly reachable from the internet for Azure Bot Service to deliver activities.
|
||||
|
||||
For local development without a public URL, use a tunnel (e.g., `ngrok http 3000`) and update the messaging endpoint in Azure Bot Configuration.
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `teams`
|
||||
- **terminology**: Teams has "teams" containing "channels." The bot can also receive DMs (personal scope) and group chat messages. Channels support threaded replies.
|
||||
- **platform-id-format**: `teams:{base64-encoded-conversation-id}:{base64-encoded-service-url}` — auto-generated by the adapter, not human-readable. Use the auto-created messaging group ID for wiring.
|
||||
- **how-to-find-id**: Send a message to the bot in the channel. NanoClaw auto-creates a messaging group and logs the platform ID. Use that messaging group ID for wiring.
|
||||
- **supports-threads**: yes (channels only; DMs and group chats are flat)
|
||||
- **typical-use**: Team collaboration with the bot in channels; personal assistant via DMs
|
||||
- **default-isolation**: Separate agent group per team. DMs can share an agent group with your main channel for unified personal memory.
|
||||
3
.claude/skills/add-teams/VERIFY.md
Normal file
3
.claude/skills/add-teams/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify Microsoft Teams Channel
|
||||
|
||||
Add the bot to a Teams channel or send it a direct message. The bot should respond within a few seconds.
|
||||
@@ -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)
|
||||
6
.claude/skills/add-telegram/REMOVE.md
Normal file
6
.claude/skills/add-telegram/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove Telegram
|
||||
|
||||
1. Comment out `import './telegram.js'` in `src/channels/index.ts`
|
||||
2. Remove `TELEGRAM_BOT_TOKEN` from `.env`
|
||||
3. `pnpm uninstall @chat-adapter/telegram`
|
||||
4. Rebuild and restart
|
||||
@@ -1,214 +1,108 @@
|
||||
---
|
||||
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).
|
||||
description: Add Telegram channel integration via Chat SDK.
|
||||
---
|
||||
|
||||
# Add Telegram Channel
|
||||
|
||||
This skill adds Telegram support to NanoClaw, then walks through interactive setup.
|
||||
Adds Telegram bot support via the Chat SDK bridge.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
## Install
|
||||
|
||||
### Check if already applied
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Telegram adapter, its formatting/pairing helpers, their tests, and the `pair-telegram` setup step in from the `channels` branch.
|
||||
|
||||
Check if `src/channels/telegram.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
### Ask the user
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
Use `AskUserQuestion` to collect configuration:
|
||||
- `src/channels/telegram.ts`, `telegram-pairing.ts`, `telegram-markdown-sanitize.ts` (and their `.test.ts` siblings) all exist
|
||||
- `src/channels/index.ts` contains `import './telegram.js';`
|
||||
- `setup/pair-telegram.ts` exists and `setup/index.ts`'s `STEPS` map contains `'pair-telegram':`
|
||||
- `@chat-adapter/telegram` is listed in `package.json` dependencies
|
||||
|
||||
AskUserQuestion: Do you have a Telegram bot token, or do you need to create one?
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
If they have one, collect it now. If not, we'll create one in Phase 3.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure channel remote
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
If `telegram` is missing, add it:
|
||||
### 2. Copy the adapter, helpers, tests, and setup step
|
||||
|
||||
```bash
|
||||
git remote add telegram https://github.com/qwibitai/nanoclaw-telegram.git
|
||||
git show origin/channels:src/channels/telegram.ts > src/channels/telegram.ts
|
||||
git show origin/channels:src/channels/telegram-pairing.ts > src/channels/telegram-pairing.ts
|
||||
git show origin/channels:src/channels/telegram-pairing.test.ts > src/channels/telegram-pairing.test.ts
|
||||
git show origin/channels:src/channels/telegram-markdown-sanitize.ts > src/channels/telegram-markdown-sanitize.ts
|
||||
git show origin/channels:src/channels/telegram-markdown-sanitize.test.ts > src/channels/telegram-markdown-sanitize.test.ts
|
||||
git show origin/channels:setup/pair-telegram.ts > setup/pair-telegram.ts
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if already present):
|
||||
|
||||
```typescript
|
||||
import './telegram.js';
|
||||
```
|
||||
|
||||
### 4. Register the setup step
|
||||
|
||||
In `setup/index.ts`, add this entry to the `STEPS` map (right after the `register` line is fine; skip if already present):
|
||||
|
||||
```typescript
|
||||
'pair-telegram': () => import('./pair-telegram.js'),
|
||||
```
|
||||
|
||||
### 5. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
git fetch telegram main
|
||||
git merge telegram/main || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
pnpm install @chat-adapter/telegram@4.26.0
|
||||
```
|
||||
|
||||
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
|
||||
### 6. Build
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/channels/telegram.test.ts
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
All tests must pass (including the new Telegram tests) and build must be clean before proceeding.
|
||||
## Credentials
|
||||
|
||||
## Phase 3: Setup
|
||||
### Create Telegram Bot
|
||||
|
||||
### Create Telegram Bot (if needed)
|
||||
1. Open Telegram and search for `@BotFather`
|
||||
2. Send `/newbot` and follow the prompts:
|
||||
- Bot name: Something friendly (e.g., "NanoClaw Assistant")
|
||||
- Bot username: Must end with "bot" (e.g., "nanoclaw_bot")
|
||||
3. Copy the bot token (looks like `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`)
|
||||
|
||||
If the user doesn't have a bot token, tell them:
|
||||
**Important for group chats**: By default, Telegram bots only see @mentions and commands in groups. To let the bot see all messages:
|
||||
|
||||
> 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.
|
||||
1. Open `@BotFather` > `/mybots` > select your bot
|
||||
2. **Bot Settings** > **Group Privacy** > **Turn off**
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
TELEGRAM_BOT_TOKEN=<their-token>
|
||||
TELEGRAM_BOT_TOKEN=your-bot-token
|
||||
```
|
||||
|
||||
Channels auto-enable when their credentials are present — no extra configuration needed.
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
Sync to container environment:
|
||||
## Next Steps
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
```
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
The container reads environment from `data/env/env`, not `.env` directly.
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
### Disable Group Privacy (for group chats)
|
||||
## Channel Info
|
||||
|
||||
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
|
||||
|
||||
The chat ID, name, and folder name are needed. Use `npx tsx setup/index.ts --step register` with the appropriate flags.
|
||||
|
||||
For a main chat (responds to all messages):
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step register -- --jid "tg:<chat-id>" --name "<chat-name>" --folder "telegram_main" --trigger "@${ASSISTANT_NAME}" --channel telegram --no-trigger-required --is-main
|
||||
```
|
||||
|
||||
For additional chats (trigger-only):
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step register -- --jid "tg:<chat-id>" --name "<chat-name>" --folder "telegram_<group-name>" --trigger "@${ASSISTANT_NAME}" --channel telegram
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## 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)
|
||||
- **type**: `telegram`
|
||||
- **terminology**: Telegram calls them "groups" and "chats." A "group" has multiple members; a "chat" is a 1:1 conversation with the bot.
|
||||
- **how-to-find-id**: Do NOT ask the user for a chat ID. Telegram registration uses pairing — run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent <main|wire-to:folder|new-agent:folder>`, show the user the 4-digit `CODE` from the `PAIR_TELEGRAM_ISSUED` block (follow the `REMINDER_TO_ASSISTANT` line in that block), and tell them to send just the 4 digits as a message from the chat they want to register (DM the bot for `main`, post in the group otherwise). In groups with Group Privacy ON, prefix with the bot handle: `@<botname> CODE`. Wrong guesses invalidate the code — if a `PAIR_TELEGRAM_ATTEMPT` block arrives with a mismatched `RECEIVED_CODE`, a `PAIR_TELEGRAM_NEW_CODE` block will follow automatically (up to 5 regenerations); show the new code. On `PAIR_TELEGRAM STATUS=failed ERROR=max-regenerations-exceeded`, ask the user if they want to try again and re-invoke the step — each invocation starts a fresh 5-attempt batch. Success emits `PAIR_TELEGRAM STATUS=success` with `PLATFORM_ID`, `IS_GROUP`, and `ADMIN_USER_ID`. The service must be running for this to work (the polling adapter is what observes the code).
|
||||
- **supports-threads**: no
|
||||
- **typical-use**: Interactive chat — direct messages or small groups
|
||||
- **default-isolation**: Same agent group if you're the only participant across multiple chats. Separate agent group if different people are in different groups.
|
||||
|
||||
3
.claude/skills/add-telegram/VERIFY.md
Normal file
3
.claude/skills/add-telegram/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify Telegram
|
||||
|
||||
Send a message to your bot in Telegram (search for its username), or add the bot to a group and send a message there. The bot should respond within a few seconds.
|
||||
147
.claude/skills/add-vercel/SKILL.md
Normal file
147
.claude/skills/add-vercel/SKILL.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
name: add-vercel
|
||||
description: Add Vercel deployment capability to NanoClaw agents. Installs the Vercel CLI in agent containers and sets up OneCLI credential injection for api.vercel.com. Use when the user wants agents to deploy web applications to Vercel.
|
||||
---
|
||||
|
||||
# Add Vercel
|
||||
|
||||
This skill gives NanoClaw agents the ability to deploy web applications to Vercel. It installs the Vercel CLI in agent containers and configures OneCLI to inject Vercel credentials automatically.
|
||||
|
||||
**Principle:** Do the work — don't tell the user to do it. Only ask for their input when it genuinely requires manual action (pasting a token).
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
Check if the container skill exists:
|
||||
|
||||
```bash
|
||||
test -d container/skills/vercel-cli && echo "INSTALLED" || echo "NOT_INSTALLED"
|
||||
```
|
||||
|
||||
If `INSTALLED`, skip to Phase 3 (Configure Credentials).
|
||||
|
||||
### Check prerequisites
|
||||
|
||||
Verify OneCLI is working (required for credential injection):
|
||||
|
||||
```bash
|
||||
onecli version 2>/dev/null && echo "ONECLI_OK" || echo "ONECLI_MISSING"
|
||||
```
|
||||
|
||||
If `ONECLI_MISSING`, tell the user to run `/init-onecli` first, then retry `/add-vercel`. Stop here.
|
||||
|
||||
## Phase 2: Install Container Skill
|
||||
|
||||
Copy the bundled container skill into the container skills directory:
|
||||
|
||||
```bash
|
||||
rsync -a .claude/skills/add-vercel/container-skills/ container/skills/
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
head -5 container/skills/vercel-cli/SKILL.md
|
||||
```
|
||||
|
||||
## Phase 3: Configure Credentials
|
||||
|
||||
### Check if Vercel credential already exists
|
||||
|
||||
```bash
|
||||
onecli secrets list 2>/dev/null | grep -i vercel
|
||||
```
|
||||
|
||||
If a Vercel credential already exists, skip to Phase 4.
|
||||
|
||||
### Set up Vercel API credential
|
||||
|
||||
The agent needs a Vercel personal access token. Tell the user:
|
||||
|
||||
> I need your Vercel personal access token. Go to https://vercel.com/account/tokens and create one with these settings:
|
||||
>
|
||||
> - **Token name:** `nanoclaw` (or any name you'll recognize)
|
||||
> - **Scope:** "Full Account" — the agent needs to create projects, deploy, and manage domains
|
||||
> - **Expiration:** "No expiration" recommended (avoids credential rotation), or pick a date if your security policy requires it
|
||||
>
|
||||
> After creating the token, copy it — you'll only see it once.
|
||||
|
||||
Once the user provides the token, add it to OneCLI:
|
||||
|
||||
```bash
|
||||
onecli secrets create \
|
||||
--name "Vercel API Token" \
|
||||
--type generic \
|
||||
--value "<TOKEN>" \
|
||||
--host-pattern "api.vercel.com" \
|
||||
--header-name "Authorization" \
|
||||
--value-format "Bearer {value}"
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
onecli secrets list | grep -i vercel
|
||||
```
|
||||
|
||||
### Assign the secret to all agents
|
||||
|
||||
OneCLI uses selective secret mode — secrets must be explicitly assigned to each agent. Get the Vercel secret ID from the output above, then assign it to every agent:
|
||||
|
||||
```bash
|
||||
# For each agent, add the Vercel secret to its assigned secrets list.
|
||||
# First get current assignments, then set them with the new secret appended.
|
||||
VERCEL_SECRET_ID=$(onecli secrets list 2>/dev/null | grep -B2 "Vercel" | grep '"id"' | head -1 | sed 's/.*"id": "//;s/".*//')
|
||||
for agent in $(onecli agents list 2>/dev/null | grep '"id"' | sed 's/.*"id": "//;s/".*//'); do
|
||||
CURRENT=$(onecli agents secrets --id "$agent" 2>/dev/null | grep '"' | grep -v hint | grep -v data | sed 's/.*"//;s/".*//' | tr '\n' ',' | sed 's/,$//')
|
||||
onecli agents set-secrets --id "$agent" --secret-ids "${CURRENT:+$CURRENT,}$VERCEL_SECRET_ID"
|
||||
done
|
||||
```
|
||||
|
||||
## Phase 4: Ensure Vercel CLI in Container Image
|
||||
|
||||
Check if `vercel` is already in the Dockerfile:
|
||||
|
||||
```bash
|
||||
grep -q 'vercel' container/Dockerfile && echo "PRESENT" || echo "MISSING"
|
||||
```
|
||||
|
||||
If `MISSING`, add `vercel` to the global npm install line in `container/Dockerfile`, then rebuild:
|
||||
|
||||
```bash
|
||||
./container/build.sh
|
||||
```
|
||||
|
||||
If `PRESENT`, skip — no rebuild needed.
|
||||
|
||||
## Phase 5: Sync Skills to Running Agent Groups
|
||||
|
||||
Container skills are copied once at group creation and not auto-synced. After installing or updating a container skill, sync it to all existing agent groups:
|
||||
|
||||
```bash
|
||||
for session_dir in data/v2-sessions/ag-*; do
|
||||
if [ -d "$session_dir/.claude-shared/skills" ]; then
|
||||
rsync -a container/skills/ "$session_dir/.claude-shared/skills/"
|
||||
echo "Synced skills to: $session_dir"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## Phase 6: Restart Running Containers
|
||||
|
||||
Stop all running agent containers so they pick up the new skills on next wake:
|
||||
|
||||
```bash
|
||||
docker ps --format "{{.ID}} {{.Names}}" | grep nanoclaw-v2 | awk '{print $1}' | xargs -r docker stop
|
||||
```
|
||||
|
||||
## Done
|
||||
|
||||
The agent can now deploy web applications to Vercel. Key commands:
|
||||
|
||||
- `vercel deploy --yes --prod --token placeholder` — deploy to production
|
||||
- `vercel ls --token placeholder` — list deployments
|
||||
- `vercel whoami --token placeholder` — check auth
|
||||
|
||||
For the full command reference, the agent has the `vercel-cli` container skill loaded automatically.
|
||||
103
.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md
Normal file
103
.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: vercel-cli
|
||||
description: Deploy apps to Vercel. Use when asked to deploy, ship, or publish a web application, or manage Vercel projects, domains, and environment variables.
|
||||
---
|
||||
|
||||
# Vercel CLI
|
||||
|
||||
You can deploy web applications to Vercel using the `vercel` CLI.
|
||||
|
||||
## Auth
|
||||
|
||||
Auth is handled by OneCLI — the HTTPS_PROXY injects the real token into API requests automatically. The Vercel CLI requires a token to be present to skip its local credential check, so **always pass `--token placeholder`** on every command. OneCLI replaces this with the real token at the proxy level.
|
||||
|
||||
Before any Vercel operation, verify auth:
|
||||
|
||||
```bash
|
||||
vercel whoami --token placeholder
|
||||
```
|
||||
|
||||
If this fails with an auth error, ask the user to add a Vercel token to OneCLI. They can create one at https://vercel.com/account/tokens and register it via `onecli secrets create` on the host. Once added, retry `vercel whoami`.
|
||||
|
||||
## Deploying
|
||||
|
||||
Always use `--yes` to skip interactive prompts and `--token placeholder` for auth (OneCLI replaces with real token).
|
||||
|
||||
```bash
|
||||
# Deploy to production
|
||||
vercel deploy --yes --prod --token placeholder
|
||||
|
||||
# Deploy from a specific directory
|
||||
vercel deploy --yes --prod --token placeholder --cwd /path/to/project
|
||||
|
||||
# Preview deployment (not production)
|
||||
vercel deploy --yes --token placeholder
|
||||
```
|
||||
|
||||
After deploying, verify the live URL:
|
||||
|
||||
```bash
|
||||
# Check deployment status
|
||||
vercel inspect <deployment-url> --token placeholder
|
||||
```
|
||||
|
||||
## Pre-Send Checks (do this before sharing the URL)
|
||||
|
||||
Don't send the deployment URL to the user until you've confirmed it's actually working. At minimum:
|
||||
|
||||
1. **Local build passes** — run `npm run build` (or the project's build command) before `vercel deploy`. If the build fails locally, fix it first; don't deploy broken code.
|
||||
2. **Deployment succeeded** — the `vercel deploy` output shows a "Production: https://..." URL and the status is READY (confirm with `vercel inspect`).
|
||||
3. **Live URL responds** — `curl -sI <url> | head -1` should return `HTTP/2 200` (or another 2xx/3xx). A 404/500 means something's broken even though Vercel reported success.
|
||||
4. **Optional visual check** — if `agent-browser` is loaded, open the URL and eyeball it. Helpful for catching broken layouts that a 200 response wouldn't reveal.
|
||||
|
||||
If any check fails, fix the issue and redeploy before reporting to the user.
|
||||
|
||||
## Project Management
|
||||
|
||||
```bash
|
||||
# Link to an existing Vercel project (non-interactive)
|
||||
vercel link --yes --token placeholder
|
||||
|
||||
# List recent deployments
|
||||
vercel ls --token placeholder
|
||||
|
||||
# List all projects
|
||||
vercel project ls --token placeholder
|
||||
```
|
||||
|
||||
## Domains
|
||||
|
||||
```bash
|
||||
# List domains
|
||||
vercel domains ls --token placeholder
|
||||
|
||||
# Add a domain to the current project
|
||||
vercel domains add example.com --token placeholder
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Pull env vars from Vercel to local .env
|
||||
vercel env pull --token placeholder
|
||||
|
||||
# Add an env var (use echo to pipe the value — avoids interactive prompt)
|
||||
echo "value" | vercel env add VAR_NAME production --token placeholder
|
||||
```
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| `Error: No framework detected` | Ensure the project has a `package.json` with a `build` script, or set the framework in `vercel.json` |
|
||||
| `Error: Rate limited` | Wait and retry. Don't loop — report to user |
|
||||
| `Error: You have reached your project limit` | User needs to upgrade Vercel plan or delete unused projects |
|
||||
| `ENOTFOUND api.vercel.com` | Network issue. Check proxy connectivity |
|
||||
| Auth error after `vercel whoami` | Credential may be expired. Ask the user to refresh the Vercel token in OneCLI |
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Run `npm run build` locally before deploying to catch build errors early
|
||||
- Use `--cwd` instead of `cd` to keep your working directory stable
|
||||
- For Next.js projects, `vercel deploy` auto-detects the framework — no extra config needed
|
||||
- Use `vercel.json` only when you need custom build settings, rewrites, or headers
|
||||
@@ -1,148 +0,0 @@
|
||||
---
|
||||
name: add-voice-transcription
|
||||
description: Add voice message transcription to NanoClaw using OpenAI's Whisper API. Automatically transcribes WhatsApp voice notes so the agent can read and respond to them.
|
||||
---
|
||||
|
||||
# Add Voice Transcription
|
||||
|
||||
This skill adds automatic voice message transcription to NanoClaw's WhatsApp channel using OpenAI's Whisper API. When a voice note arrives, it is downloaded, transcribed, and delivered to the agent as `[Voice: <transcript>]`.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
Check if `src/transcription.ts` exists. If it does, skip to Phase 3 (Configure). The code changes are already in place.
|
||||
|
||||
### Ask the user
|
||||
|
||||
Use `AskUserQuestion` to collect information:
|
||||
|
||||
AskUserQuestion: Do you have an OpenAI API key for Whisper transcription?
|
||||
|
||||
If yes, collect it now. If no, direct them to create one at https://platform.openai.com/api-keys.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
**Prerequisite:** WhatsApp must be installed first (`skill/whatsapp` merged). This skill modifies WhatsApp channel files.
|
||||
|
||||
### 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/voice-transcription
|
||||
git merge whatsapp/skill/voice-transcription || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `src/transcription.ts` (voice transcription module using OpenAI Whisper)
|
||||
- Voice handling in `src/channels/whatsapp.ts` (isVoiceMessage check, transcribeAudioMessage call)
|
||||
- Transcription tests in `src/channels/whatsapp.test.ts`
|
||||
- `openai` npm dependency in `package.json`
|
||||
- `OPENAI_API_KEY` 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 --legacy-peer-deps
|
||||
npm run build
|
||||
npx vitest run src/channels/whatsapp.test.ts
|
||||
```
|
||||
|
||||
All tests must pass and build must be clean before proceeding.
|
||||
|
||||
## Phase 3: Configure
|
||||
|
||||
### Get OpenAI API key (if needed)
|
||||
|
||||
If the user doesn't have an API key:
|
||||
|
||||
> I need you to create an OpenAI API key:
|
||||
>
|
||||
> 1. Go to https://platform.openai.com/api-keys
|
||||
> 2. Click "Create new secret key"
|
||||
> 3. Give it a name (e.g., "NanoClaw Transcription")
|
||||
> 4. Copy the key (starts with `sk-`)
|
||||
>
|
||||
> Cost: ~$0.006 per minute of audio (~$0.003 per typical 30-second voice note)
|
||||
|
||||
Wait for the user to provide the key.
|
||||
|
||||
### Add to environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
OPENAI_API_KEY=<their-key>
|
||||
```
|
||||
|
||||
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 # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## Phase 4: Verify
|
||||
|
||||
### Test with a voice note
|
||||
|
||||
Tell the user:
|
||||
|
||||
> Send a voice note in any registered WhatsApp chat. The agent should receive it as `[Voice: <transcript>]` and respond to its content.
|
||||
|
||||
### Check logs if needed
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log | grep -i voice
|
||||
```
|
||||
|
||||
Look for:
|
||||
- `Transcribed voice message` — successful transcription with character count
|
||||
- `OPENAI_API_KEY not set` — key missing from `.env`
|
||||
- `OpenAI transcription failed` — API error (check key validity, billing)
|
||||
- `Failed to download audio message` — media download issue
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Voice notes show "[Voice Message - transcription unavailable]"
|
||||
|
||||
1. Check `OPENAI_API_KEY` is set in `.env` AND synced to `data/env/env`
|
||||
2. Verify key works: `curl -s https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY" | head -c 200`
|
||||
3. Check OpenAI billing — Whisper requires a funded account
|
||||
|
||||
### Voice notes show "[Voice Message - transcription failed]"
|
||||
|
||||
Check logs for the specific error. Common causes:
|
||||
- Network timeout — transient, will work on next message
|
||||
- Invalid API key — regenerate at https://platform.openai.com/api-keys
|
||||
- Rate limiting — wait and retry
|
||||
|
||||
### Agent doesn't respond to voice notes
|
||||
|
||||
Verify the chat is registered and the agent is running. Voice transcription only runs for registered groups.
|
||||
6
.claude/skills/add-webex/REMOVE.md
Normal file
6
.claude/skills/add-webex/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove Webex Channel
|
||||
|
||||
1. Comment out `import './webex.js'` in `src/channels/index.ts`
|
||||
2. Remove `WEBEX_BOT_TOKEN` and `WEBEX_WEBHOOK_SECRET` from `.env`
|
||||
3. `pnpm uninstall @bitbasti/chat-adapter-webex`
|
||||
4. Rebuild and restart
|
||||
88
.claude/skills/add-webex/SKILL.md
Normal file
88
.claude/skills/add-webex/SKILL.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: add-webex
|
||||
description: Add Webex channel integration via Chat SDK.
|
||||
---
|
||||
|
||||
# Add Webex Channel
|
||||
|
||||
Adds Cisco Webex support via the Chat SDK bridge.
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Webex adapter in from the `channels` branch.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/webex.ts` exists
|
||||
- `src/channels/index.ts` contains `import './webex.js';`
|
||||
- `@bitbasti/chat-adapter-webex` is listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/webex.ts > src/channels/webex.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './webex.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @bitbasti/chat-adapter-webex@0.1.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
1. Go to [developer.webex.com](https://developer.webex.com/my-apps/new/bot) and create a new bot
|
||||
2. Copy the **Bot Access Token**
|
||||
3. Set up a webhook:
|
||||
- Use the Webex API or Developer Portal to create a webhook pointing to `https://your-domain/webhook/webex`
|
||||
- Set a webhook secret for signature verification
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
WEBEX_BOT_TOKEN=your-bot-token
|
||||
WEBEX_WEBHOOK_SECRET=your-webhook-secret
|
||||
```
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `webex`
|
||||
- **terminology**: Webex has "spaces." A space can be a group conversation or a 1:1 direct message with the bot.
|
||||
- **how-to-find-id**: Open the space in Webex, click the space name > Settings — the Space ID is listed there. Or use the Webex API (`GET /rooms`) to list spaces and their IDs.
|
||||
- **supports-threads**: yes
|
||||
- **typical-use**: Interactive chat — team spaces or direct messages
|
||||
- **default-isolation**: Same agent group for spaces where you're the primary user. Separate agent group for spaces with different teams or sensitive information.
|
||||
3
.claude/skills/add-webex/VERIFY.md
Normal file
3
.claude/skills/add-webex/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify Webex Channel
|
||||
|
||||
Add the bot to a Webex space or send it a direct message. The bot should respond within a few seconds.
|
||||
49
.claude/skills/add-wechat/REMOVE.md
Normal file
49
.claude/skills/add-wechat/REMOVE.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Remove WeChat Channel
|
||||
|
||||
Undo `/add-wechat`.
|
||||
|
||||
### 1. Remove credentials
|
||||
|
||||
Delete WeChat lines from `.env`:
|
||||
|
||||
```bash
|
||||
sed -i.bak '/^WECHAT_ENABLED=/d' .env && rm -f .env.bak
|
||||
cp .env data/env/env
|
||||
```
|
||||
|
||||
### 2. Remove adapter and import
|
||||
|
||||
```bash
|
||||
rm -f src/channels/wechat.ts
|
||||
sed -i.bak "/import '\.\/wechat\.js';/d" src/channels/index.ts && rm -f src/channels/index.ts.bak
|
||||
```
|
||||
|
||||
### 3. Uninstall the package
|
||||
|
||||
```bash
|
||||
pnpm remove wechat-ilink-client
|
||||
```
|
||||
|
||||
### 4. Remove saved auth + sync state
|
||||
|
||||
```bash
|
||||
rm -rf data/wechat
|
||||
```
|
||||
|
||||
### 5. Remove DB wiring
|
||||
|
||||
```sql
|
||||
-- Remove any sessions first (foreign key)
|
||||
DELETE FROM sessions WHERE messaging_group_id IN (SELECT id FROM messaging_groups WHERE channel_type = 'wechat');
|
||||
DELETE FROM messaging_group_agents WHERE messaging_group_id IN (SELECT id FROM messaging_groups WHERE channel_type = 'wechat');
|
||||
DELETE FROM messaging_groups WHERE channel_type = 'wechat';
|
||||
```
|
||||
|
||||
### 6. Rebuild and restart
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
systemctl --user restart nanoclaw # Linux
|
||||
# or
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
```
|
||||
170
.claude/skills/add-wechat/SKILL.md
Normal file
170
.claude/skills/add-wechat/SKILL.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: add-wechat
|
||||
description: Add WeChat (personal) channel integration via Tencent's official iLink Bot API. Uses long-polling and QR scan — no webhook, no ToS risk, no paid token.
|
||||
---
|
||||
|
||||
# Add WeChat Channel
|
||||
|
||||
Adds WeChat support via **iLink Bot API** — the first-party Tencent API for personal WeChat bots (different from WeCom / Official Account).
|
||||
|
||||
**Why this is different from wechaty/PadLocal:**
|
||||
|
||||
- Official Tencent API — no ToS violation, no ban risk
|
||||
- Free — no PadLocal token required
|
||||
- No public webhook URL needed — uses long-poll
|
||||
- Works with any personal WeChat account
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A **personal WeChat account** with the mobile app installed
|
||||
- A phone to scan the QR code for login
|
||||
- Node.js >= 20 (already required by NanoClaw)
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the WeChat adapter in from the `channels` branch.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/wechat.ts` exists
|
||||
- `src/channels/index.ts` contains `import './wechat.js';`
|
||||
- `wechat-ilink-client` is listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/wechat.ts > src/channels/wechat.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './wechat.js';
|
||||
```
|
||||
|
||||
### 4. Install the library (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install wechat-ilink-client@0.1.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
Unlike most channels, WeChat requires **no pre-configured API keys**. Auth happens via QR code scan from your phone.
|
||||
|
||||
### 1. Enable the channel
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
WECHAT_ENABLED=true
|
||||
```
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
### 2. Start the service and scan the QR
|
||||
|
||||
Restart NanoClaw:
|
||||
|
||||
```bash
|
||||
systemctl --user restart nanoclaw # Linux
|
||||
# or
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
```
|
||||
|
||||
The adapter will print a **QR URL** to the logs and save it to `data/wechat/qr.txt`:
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log | grep WeChat
|
||||
# or
|
||||
cat data/wechat/qr.txt
|
||||
```
|
||||
|
||||
Open the URL in a browser (it renders a QR code), then:
|
||||
|
||||
1. Open WeChat on your phone
|
||||
2. Use its built-in QR scanner (top-right "+" → Scan)
|
||||
3. Approve the authorization on your phone
|
||||
4. Auth credentials are saved to `data/wechat/auth.json` — do not commit this file
|
||||
|
||||
The bot is now connected as your WeChat account.
|
||||
|
||||
## Wire your first DM
|
||||
|
||||
A successful QR login alone isn't enough — the adapter still needs to be wired to an agent group before it can respond.
|
||||
|
||||
### 1. Trigger the first inbound message
|
||||
|
||||
Have a different WeChat account send a message to the bot account. This auto-creates a `messaging_groups` row with the sender's `platform_id`.
|
||||
|
||||
### 2. Run the wire script
|
||||
|
||||
```bash
|
||||
pnpm exec tsx .claude/skills/add-wechat/scripts/wire-dm.ts
|
||||
```
|
||||
|
||||
Interactive flow: the script lists all unwired WeChat messaging groups, asks which agent group to wire it to, and creates the `messaging_group_agents` row with sensible defaults (sender policy `request_approval`, session mode `shared`).
|
||||
|
||||
With `request_approval`, the next DM from the stranger fires an approval card to the admin — admin taps Approve/Deny, approved users are added as members and their queued message replays through the agent.
|
||||
|
||||
Non-interactive:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx .claude/skills/add-wechat/scripts/wire-dm.ts \
|
||||
--platform-id wechat:wxid_xxxxx \
|
||||
--agent-group ag-xxxxx \
|
||||
--non-interactive
|
||||
```
|
||||
|
||||
Flags:
|
||||
|
||||
- `--platform-id <id>` — wire a specific messaging group (default: most recent unwired)
|
||||
- `--agent-group <id>` — target agent group (default: prompt; or solo admin group in non-interactive)
|
||||
- `--sender-policy public|strict|request_approval` — default `request_approval` (fires an admin approval card on unknown-sender DMs)
|
||||
- `--session-mode shared|per-thread` — default `shared`
|
||||
|
||||
### 3. Test
|
||||
|
||||
Have the sender message the bot again — the agent should respond.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- **Only one instance can use a given token at a time.** Don't run multiple NanoClaw instances pointing to the same `data/wechat/auth.json`.
|
||||
- **Re-login on session expiry:** if you see `WeChat: session expired` in logs, delete `data/wechat/auth.json` and restart — you'll be asked to re-scan.
|
||||
- **Sync cursor persistence:** `data/wechat/sync-buf.txt` holds the long-poll cursor. Deleting it replays recent history on next start; don't delete it in normal operation.
|
||||
- **Account safety:** this uses the official Tencent API, so account bans for bot automation aren't a risk. That said, don't spam — normal rate limits still apply.
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, restart the service to pick up the new channel and wiring.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `wechat`
|
||||
- **terminology**: WeChat has "contacts" (DMs) and "group chats" (rooms). Each DM or group is a separate messaging group.
|
||||
- **how-to-find-id**: Send a message to the bot from the target account; the adapter auto-creates a messaging group and logs `WeChat inbound platformId=wechat:<id>`. Use `wechat:<user_id>` for DMs, `wechat:<group_id>` for rooms.
|
||||
- **admin-user-id**: The operator's WeChat user_id (for `init-first-agent.ts --admin-user-id`) is saved to `data/wechat/auth.json` as `operatorUserId` after the QR scan. Read it with `cat data/wechat/auth.json | jq -r .operatorUserId` and prefix with `wechat:` (i.e. `wechat:<operatorUserId>`).
|
||||
- **supports-threads**: no (WeChat has no reply threads)
|
||||
- **typical-use**: Long-poll — the adapter holds a persistent connection to Tencent's iLink API and receives messages in real time. No webhook URL needed.
|
||||
- **default-isolation**: `shared` session mode per messaging group (DM or room). Use `strict` sender policy if you want only specific users to reach the agent; `public` opens it to anyone who messages the bot.
|
||||
- **post-install-wiring**: Use the `wire-dm.ts` helper (see the "Wire your first DM" section above) if running this skill standalone. If running inside `/new-setup`, `init-first-agent.ts` handles wiring — just pass the `platform-id` and `admin-user-id` captured above.
|
||||
172
.claude/skills/add-wechat/scripts/wire-dm.ts
Normal file
172
.claude/skills/add-wechat/scripts/wire-dm.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env pnpm exec tsx
|
||||
/**
|
||||
* Wire a WeChat DM (or group) to an agent group.
|
||||
*
|
||||
* After /add-wechat installs the adapter and the user scans the QR login,
|
||||
* the first inbound message from another WeChat account auto-creates a
|
||||
* `messaging_groups` row. This script finds that row, asks the operator
|
||||
* which agent group to wire it to, and inserts the `messaging_group_agents`
|
||||
* join row with sensible defaults — the "post-login wiring" step /add-wechat
|
||||
* otherwise requires manual SQL for.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm exec tsx .claude/skills/add-wechat/scripts/wire-dm.ts
|
||||
*
|
||||
* Flags:
|
||||
* --platform-id <id> Wire a specific messaging group (default: most recent unwired)
|
||||
* --agent-group <id> Target agent group (default: interactive pick; or solo admin group)
|
||||
* --sender-policy <p> public | strict (default: public)
|
||||
* --session-mode <m> shared | per-thread (default: shared)
|
||||
* --non-interactive Fail instead of prompting
|
||||
*/
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
|
||||
const DB_PATH = process.env.NANOCLAW_DB_PATH ?? path.join(process.cwd(), 'data', 'v2.db');
|
||||
|
||||
type SenderPolicy = 'public' | 'strict' | 'request_approval';
|
||||
|
||||
interface Args {
|
||||
platformId?: string;
|
||||
agentGroupId?: string;
|
||||
senderPolicy: SenderPolicy;
|
||||
sessionMode: 'shared' | 'per-thread';
|
||||
interactive: boolean;
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): Args {
|
||||
const args: Args = {
|
||||
// Default matches the router's auto-create (`request_approval`) so the
|
||||
// admin gets an approval card on the next unknown-sender DM rather than
|
||||
// a silent allow. Pass `--sender-policy public` to open the channel to
|
||||
// anyone, or `strict` to require explicit membership.
|
||||
senderPolicy: 'request_approval',
|
||||
sessionMode: 'shared',
|
||||
interactive: true,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const flag = argv[i];
|
||||
const val = argv[i + 1];
|
||||
switch (flag) {
|
||||
case '--platform-id': args.platformId = val; i++; break;
|
||||
case '--agent-group': args.agentGroupId = val; i++; break;
|
||||
case '--sender-policy':
|
||||
if (val !== 'public' && val !== 'strict' && val !== 'request_approval') {
|
||||
throw new Error(`bad --sender-policy: ${val} (use public | strict | request_approval)`);
|
||||
}
|
||||
args.senderPolicy = val; i++; break;
|
||||
case '--session-mode':
|
||||
if (val !== 'shared' && val !== 'per-thread') throw new Error(`bad --session-mode: ${val}`);
|
||||
args.sessionMode = val; i++; break;
|
||||
case '--non-interactive': args.interactive = false; break;
|
||||
case '--help': case '-h':
|
||||
console.log('See .claude/skills/add-wechat/scripts/wire-dm.ts header for usage.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
async function prompt(q: string): Promise<string> {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
return new Promise((resolve) => rl.question(q, (a) => { rl.close(); resolve(a.trim()); }));
|
||||
}
|
||||
|
||||
function generateId(prefix: string): string {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const db = new Database(DB_PATH);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// 1. Pick the messaging group
|
||||
let platformId = args.platformId;
|
||||
if (!platformId) {
|
||||
const rows = db.prepare(`
|
||||
SELECT mg.id, mg.platform_id, mg.name, mg.is_group, mg.created_at
|
||||
FROM messaging_groups mg
|
||||
LEFT JOIN messaging_group_agents mga ON mga.messaging_group_id = mg.id
|
||||
WHERE mg.channel_type = 'wechat' AND mga.id IS NULL
|
||||
ORDER BY mg.created_at DESC
|
||||
`).all() as Array<{ id: string; platform_id: string; name: string | null; is_group: number; created_at: string }>;
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.error('No unwired WeChat messaging groups found.');
|
||||
console.error('Send a message to the bot first (from another WeChat account), then re-run.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (rows.length === 1 || !args.interactive) {
|
||||
platformId = rows[0].platform_id;
|
||||
console.log(`Using most recent unwired group: ${platformId} (${rows[0].is_group ? 'group' : 'DM'})`);
|
||||
} else {
|
||||
console.log('Unwired WeChat messaging groups:');
|
||||
rows.forEach((r, i) => {
|
||||
console.log(` ${i + 1}. ${r.platform_id} (${r.is_group ? 'group' : 'DM'}, ${r.created_at})`);
|
||||
});
|
||||
const pick = await prompt('Pick one [1]: ');
|
||||
const idx = pick === '' ? 0 : parseInt(pick, 10) - 1;
|
||||
if (Number.isNaN(idx) || idx < 0 || idx >= rows.length) throw new Error('invalid choice');
|
||||
platformId = rows[idx].platform_id;
|
||||
}
|
||||
}
|
||||
|
||||
const mg = db.prepare(
|
||||
'SELECT id, platform_id, is_group FROM messaging_groups WHERE channel_type = ? AND platform_id = ?'
|
||||
).get('wechat', platformId) as { id: string; platform_id: string; is_group: number } | undefined;
|
||||
if (!mg) throw new Error(`no wechat messaging_group with platform_id = ${platformId}`);
|
||||
|
||||
// 2. Pick the agent group
|
||||
let agentGroupId = args.agentGroupId;
|
||||
if (!agentGroupId) {
|
||||
const agents = db.prepare('SELECT id, name, is_admin FROM agent_groups ORDER BY is_admin DESC, created_at ASC')
|
||||
.all() as Array<{ id: string; name: string; is_admin: number }>;
|
||||
if (agents.length === 0) throw new Error('no agent groups exist — create one first');
|
||||
|
||||
const adminAgents = agents.filter((a) => a.is_admin === 1);
|
||||
if (adminAgents.length === 1 && !args.interactive) {
|
||||
agentGroupId = adminAgents[0].id;
|
||||
console.log(`Auto-selected sole admin agent group: ${adminAgents[0].name} (${agentGroupId})`);
|
||||
} else if (args.interactive) {
|
||||
console.log('Agent groups:');
|
||||
agents.forEach((a, i) => {
|
||||
console.log(` ${i + 1}. ${a.name} (${a.id})${a.is_admin ? ' [admin]' : ''}`);
|
||||
});
|
||||
const pick = await prompt('Pick one [1]: ');
|
||||
const idx = pick === '' ? 0 : parseInt(pick, 10) - 1;
|
||||
if (Number.isNaN(idx) || idx < 0 || idx >= agents.length) throw new Error('invalid choice');
|
||||
agentGroupId = agents[idx].id;
|
||||
} else {
|
||||
throw new Error('multiple agent groups exist; pass --agent-group <id>');
|
||||
}
|
||||
}
|
||||
|
||||
const ag = db.prepare('SELECT id, name FROM agent_groups WHERE id = ?').get(agentGroupId) as
|
||||
{ id: string; name: string } | undefined;
|
||||
if (!ag) throw new Error(`no agent_group with id = ${agentGroupId}`);
|
||||
|
||||
// 3. Update sender policy + wire
|
||||
const tx = db.transaction(() => {
|
||||
db.prepare('UPDATE messaging_groups SET unknown_sender_policy = ? WHERE id = ?')
|
||||
.run(args.senderPolicy, mg.id);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO messaging_group_agents
|
||||
(id, messaging_group_id, agent_group_id, trigger_rules, response_scope, session_mode, priority, created_at)
|
||||
VALUES (?, ?, ?, '', 'all', ?, 10, datetime('now'))
|
||||
`).run(generateId('mga'), mg.id, ag.id, args.sessionMode);
|
||||
});
|
||||
tx();
|
||||
|
||||
console.log('');
|
||||
console.log(`WIRED platform_id=${mg.platform_id} agent_group=${ag.name} policy=${args.senderPolicy} mode=${args.sessionMode}`);
|
||||
db.close();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('FAILED:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
6
.claude/skills/add-whatsapp-cloud/REMOVE.md
Normal file
6
.claude/skills/add-whatsapp-cloud/REMOVE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Remove WhatsApp Cloud API Channel
|
||||
|
||||
1. Comment out `import './whatsapp-cloud.js'` in `src/channels/index.ts`
|
||||
2. Remove `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_APP_SECRET`, `WHATSAPP_VERIFY_TOKEN` from `.env`
|
||||
3. `pnpm uninstall @chat-adapter/whatsapp`
|
||||
4. Rebuild and restart
|
||||
95
.claude/skills/add-whatsapp-cloud/SKILL.md
Normal file
95
.claude/skills/add-whatsapp-cloud/SKILL.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: add-whatsapp-cloud
|
||||
description: Add WhatsApp Business Cloud API channel via Chat SDK. Official Meta API.
|
||||
---
|
||||
|
||||
# Add WhatsApp Cloud API Channel
|
||||
|
||||
Connect NanoClaw to WhatsApp via the official Meta WhatsApp Business Cloud API.
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the WhatsApp Cloud adapter in from the `channels` branch.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/whatsapp-cloud.ts` exists
|
||||
- `src/channels/index.ts` contains `import './whatsapp-cloud.js';`
|
||||
- `@chat-adapter/whatsapp` is listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/whatsapp-cloud.ts > src/channels/whatsapp-cloud.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if the line is already present):
|
||||
|
||||
```typescript
|
||||
import './whatsapp-cloud.js';
|
||||
```
|
||||
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/whatsapp@4.26.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
1. Go to [Meta for Developers](https://developers.facebook.com/apps/) and create an app (type: Business).
|
||||
2. Add the **WhatsApp** product.
|
||||
3. Go to **WhatsApp** > **API Setup**:
|
||||
- Note the **Phone Number ID** (not the phone number itself).
|
||||
- Generate a **permanent System User access token** with `whatsapp_business_messaging` permission.
|
||||
4. Go to **WhatsApp** > **Configuration**:
|
||||
- Set webhook URL: `https://your-domain/webhook/whatsapp`.
|
||||
- Set a **Verify Token** (any random string you choose).
|
||||
- Subscribe to webhook fields: `messages`.
|
||||
5. Copy the **App Secret** from **Settings** > **Basic**.
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
WHATSAPP_ACCESS_TOKEN=your-system-user-access-token
|
||||
WHATSAPP_PHONE_NUMBER_ID=your-phone-number-id
|
||||
WHATSAPP_APP_SECRET=your-app-secret
|
||||
WHATSAPP_VERIFY_TOKEN=your-verify-token
|
||||
```
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
## Channel Info
|
||||
|
||||
- **type**: `whatsapp-cloud`
|
||||
- **terminology**: WhatsApp Cloud API supports 1:1 conversations only (no group chats). Each conversation is with a phone number.
|
||||
- **how-to-find-id**: The platform ID is the Phone Number ID from the Meta Business dashboard (not the phone number itself). Find it under WhatsApp > API Setup.
|
||||
- **supports-threads**: no
|
||||
- **typical-use**: Interactive 1:1 chat -- direct messages only
|
||||
- **default-isolation**: Same agent group if you're the only person messaging the bot. Each additional person who messages gets their own conversation automatically, but they share the agent's workspace and memory -- use a separate agent group if you need information isolation between different contacts.
|
||||
3
.claude/skills/add-whatsapp-cloud/VERIFY.md
Normal file
3
.claude/skills/add-whatsapp-cloud/VERIFY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verify WhatsApp Cloud API Channel
|
||||
|
||||
Send a message to your WhatsApp Business number. The bot should respond within a few seconds. Note: WhatsApp Cloud API only supports 1:1 DMs, not group chats.
|
||||
@@ -1,20 +1,81 @@
|
||||
---
|
||||
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.
|
||||
description: Add WhatsApp channel via native Baileys adapter. Direct connection — no Chat SDK bridge. 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.
|
||||
Adds WhatsApp support via the native Baileys adapter (no Chat SDK bridge).
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the native WhatsApp (Baileys) adapter and its `whatsapp-auth` setup step in from the `channels` branch. No Chat SDK bridge.
|
||||
|
||||
### Pre-flight (idempotent)
|
||||
|
||||
Skip to **Credentials** if all of these are already in place:
|
||||
|
||||
- `src/channels/whatsapp.ts` exists
|
||||
- `src/channels/index.ts` contains `import './whatsapp.js';`
|
||||
- `setup/whatsapp-auth.ts` and `setup/groups.ts` both exist
|
||||
- `setup/index.ts`'s `STEPS` map contains both `'whatsapp-auth':` and `groups:`
|
||||
- `@whiskeysockets/baileys`, `qrcode`, `pino` are listed in `package.json` dependencies
|
||||
|
||||
Otherwise continue. Every step below is safe to re-run.
|
||||
|
||||
### 1. Fetch the channels branch
|
||||
|
||||
```bash
|
||||
git fetch origin channels
|
||||
```
|
||||
|
||||
### 2. Copy the adapter and setup steps
|
||||
|
||||
```bash
|
||||
git show origin/channels:src/channels/whatsapp.ts > src/channels/whatsapp.ts
|
||||
git show origin/channels:setup/whatsapp-auth.ts > setup/whatsapp-auth.ts
|
||||
git show origin/channels:setup/groups.ts > setup/groups.ts
|
||||
```
|
||||
|
||||
### 3. Append the self-registration import
|
||||
|
||||
Append to `src/channels/index.ts` (skip if already present):
|
||||
|
||||
```typescript
|
||||
import './whatsapp.js';
|
||||
```
|
||||
|
||||
### 4. Register the setup steps
|
||||
|
||||
In `setup/index.ts`, add these entries to the `STEPS` map (skip lines already present):
|
||||
|
||||
```typescript
|
||||
groups: () => import('./groups.js'),
|
||||
'whatsapp-auth': () => import('./whatsapp-auth.js'),
|
||||
```
|
||||
|
||||
### 5. Install the adapter packages (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @whiskeysockets/baileys@6.17.16 qrcode@1.5.4 @types/qrcode@1.5.6 pino@9.6.0
|
||||
```
|
||||
|
||||
### 6. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
WhatsApp uses linked-device authentication — no API key, just a one-time pairing from your phone.
|
||||
|
||||
### 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).
|
||||
Check if WhatsApp is already authenticated. If `store/auth/creds.json` exists, skip to "Shared vs dedicated number".
|
||||
|
||||
```bash
|
||||
ls store/auth/creds.json 2>/dev/null && echo "WhatsApp auth exists" || echo "No WhatsApp auth"
|
||||
test -f store/auth/creds.json && echo "WhatsApp auth exists" || echo "No WhatsApp auth"
|
||||
```
|
||||
|
||||
### Detect environment
|
||||
@@ -42,57 +103,6 @@ If they chose pairing code:
|
||||
|
||||
AskUserQuestion: What is your phone number? (Digits only — country code followed by your 10-digit number, no + prefix, spaces, or dashes. Example: 14155551234 where 1 is the US country code and 4155551234 is the phone number.)
|
||||
|
||||
## 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 || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
@@ -104,7 +114,7 @@ rm -rf store/auth/
|
||||
For QR code in browser (recommended):
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
|
||||
pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
|
||||
```
|
||||
|
||||
(Bash timeout: 150000ms)
|
||||
@@ -120,10 +130,12 @@ Tell the user:
|
||||
For QR code in terminal:
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step whatsapp-auth -- --method qr-terminal
|
||||
pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-terminal
|
||||
```
|
||||
|
||||
Tell the user to run `npm run auth` in another terminal, then:
|
||||
(Bash timeout: 150000ms)
|
||||
|
||||
Tell the user:
|
||||
|
||||
> 1. Open WhatsApp > **Settings** > **Linked Devices** > **Link a Device**
|
||||
> 2. Scan the QR code displayed in the terminal
|
||||
@@ -135,7 +147,7 @@ Tell the user to have WhatsApp open on **Settings > Linked Devices > Link a Devi
|
||||
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 &
|
||||
rm -f store/pairing-code.txt && pnpm exec 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):
|
||||
@@ -155,10 +167,10 @@ Display the code to the user the moment it appears. Tell them:
|
||||
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
|
||||
for i in $(seq 1 60); do grep -q 'STATUS: authenticated' /tmp/wa-auth.log 2>/dev/null && echo "authenticated" && break; grep -q '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.
|
||||
**If failed:** logged_out → delete `store/auth/` and re-run. timeout → ask user, offer retry.
|
||||
|
||||
### Verify authentication succeeded
|
||||
|
||||
@@ -166,128 +178,43 @@ for i in $(seq 1 60); do grep -q 'AUTH_STATUS: authenticated' /tmp/wa-auth.log 2
|
||||
test -f store/auth/creds.json && echo "Authentication successful" || echo "Authentication failed"
|
||||
```
|
||||
|
||||
### Configure environment
|
||||
### Shared vs dedicated number
|
||||
|
||||
Channels auto-enable when their credentials are present — WhatsApp activates when `store/auth/creds.json` exists.
|
||||
AskUserQuestion: Is this a shared phone number (personal WhatsApp) or a dedicated number?
|
||||
- **Shared number** — your personal WhatsApp (bot prefixes messages with its name)
|
||||
- **Dedicated number** — a separate phone/SIM for the assistant
|
||||
|
||||
Sync to container environment:
|
||||
If dedicated, add to `.env`:
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
ASSISTANT_HAS_OWN_NUMBER=true
|
||||
```
|
||||
|
||||
## Phase 4: Registration
|
||||
## Next Steps
|
||||
|
||||
### Configure trigger and channel type
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
Get the bot's WhatsApp number: `node -e "const c=require('./store/auth/creds.json');console.log(c.me.id.split(':')[0].split('@')[0])"`
|
||||
Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
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
|
||||
## Channel Info
|
||||
|
||||
AskUserQuestion: What trigger word should activate the assistant?
|
||||
- **@Andy** - Default trigger
|
||||
- **@Claw** - Short and easy
|
||||
- **@Claude** - Match the AI name
|
||||
- **type**: `whatsapp`
|
||||
- **terminology**: WhatsApp calls them "groups" and "chats." A "chat" is a 1:1 DM; a "group" has multiple members.
|
||||
- **how-to-find-id**: DMs use `<phone>@s.whatsapp.net` (e.g. `14155551234@s.whatsapp.net`). Groups use `<id>@g.us`. To find your number: `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')"`. Groups are auto-discovered — check `sqlite3 data/v2.db "SELECT platform_id, name FROM messaging_groups WHERE channel_type='whatsapp' AND is_group=1"`.
|
||||
- **supports-threads**: no
|
||||
- **typical-use**: Interactive chat — direct messages or small groups
|
||||
- **default-isolation**: Same agent group if you're the only participant across multiple chats. Separate agent group if different people are in different groups.
|
||||
|
||||
AskUserQuestion: What should the assistant call itself?
|
||||
- **Andy** - Default name
|
||||
- **Claw** - Short and easy
|
||||
- **Claude** - Match the AI name
|
||||
### Features
|
||||
|
||||
AskUserQuestion: Where do you want to chat with the assistant?
|
||||
- Markdown formatting — `**bold**`→`*bold*`, `*italic*`→`_italic_`, headings→bold, code blocks preserved
|
||||
- Approval questions — `ask_user_question` renders with `/approve`, `/reject` slash commands
|
||||
- File attachments — send and receive images, video, audio, documents
|
||||
- Reactions — send emoji reactions on messages
|
||||
- Typing indicators — composing presence updates
|
||||
- Credential requests — text fallback (WhatsApp has no modal support)
|
||||
|
||||
**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
|
||||
```
|
||||
Not supported (WhatsApp linked device limitation): edit messages, delete messages.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -296,77 +223,42 @@ tail -f logs/nanoclaw.log
|
||||
QR codes expire after ~60 seconds. Re-run the auth command:
|
||||
|
||||
```bash
|
||||
rm -rf store/auth/ && npx tsx src/whatsapp-auth.ts
|
||||
rm -rf store/auth/ && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
|
||||
```
|
||||
|
||||
### Pairing code not working
|
||||
|
||||
Codes expire in ~60 seconds. To retry:
|
||||
Codes expire in ~60 seconds. Delete auth and retry:
|
||||
|
||||
```bash
|
||||
rm -rf store/auth/ && npx tsx src/whatsapp-auth.ts --pairing-code --phone <phone>
|
||||
rm -rf store/auth/ && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method pairing-code --phone <phone>
|
||||
```
|
||||
|
||||
Enter the code **immediately** when it appears. Also ensure:
|
||||
1. Phone number is digits only — country code + number, no `+` prefix (e.g., `14155551234` where `1` is country code, `4155551234` is the number)
|
||||
2. Phone has internet access
|
||||
3. WhatsApp is updated to the latest version
|
||||
Ensure: digits only (no `+`), phone has internet, WhatsApp is updated.
|
||||
|
||||
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
|
||||
rm -rf store/auth/ && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
|
||||
```
|
||||
|
||||
### "conflict" disconnection
|
||||
### "waiting for this message" on reactions
|
||||
|
||||
This happens when two instances connect with the same credentials. Ensure only one NanoClaw process is running:
|
||||
Signal sessions corrupted from rapid restarts. Clear sessions:
|
||||
|
||||
```bash
|
||||
pkill -f "node dist/index.js"
|
||||
# Then restart
|
||||
systemctl --user stop nanoclaw
|
||||
rm store/auth/session-*.json
|
||||
systemctl --user start nanoclaw
|
||||
```
|
||||
|
||||
### 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`
|
||||
1. Auth exists: `test -f store/auth/creds.json`
|
||||
2. Connected: `grep "Connected to WhatsApp" logs/nanoclaw.log | tail -1`
|
||||
3. Channel wired: `sqlite3 data/v2.db "SELECT mg.platform_id, mg.name FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id=mga.messaging_group_id WHERE mg.channel_type='whatsapp'"`
|
||||
4. Service running: `systemctl --user status nanoclaw`
|
||||
|
||||
### Group names not showing
|
||||
### "conflict" disconnection
|
||||
|
||||
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)
|
||||
Two instances connected with same credentials. Ensure only one NanoClaw process is running.
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
---
|
||||
name: channel-formatting
|
||||
description: Convert Claude's Markdown output to each channel's native text syntax before delivery. Adds zero-dependency formatting for WhatsApp, Telegram, and Slack (marker substitution). Also ships a Signal rich-text helper (parseSignalStyles) used by the Signal skill.
|
||||
---
|
||||
|
||||
# Channel Formatting
|
||||
|
||||
This skill wires channel-aware Markdown conversion into the outbound pipeline so Claude's
|
||||
responses render natively on each platform — no more literal `**asterisks**` in WhatsApp or
|
||||
Telegram.
|
||||
|
||||
| Channel | Transformation |
|
||||
|---------|---------------|
|
||||
| WhatsApp | `**bold**` → `*bold*`, `*italic*` → `_italic_`, headings → bold, links → `text (url)` |
|
||||
| Telegram | same as WhatsApp, but `[text](url)` links are preserved (Markdown v1 renders them natively) |
|
||||
| Slack | same as WhatsApp, but links become `<url\|text>` |
|
||||
| Discord | passthrough (Discord already renders Markdown) |
|
||||
| Signal | passthrough for `parseTextStyles`; `parseSignalStyles` in `src/text-styles.ts` produces plain text + native `textStyle` ranges for use by the Signal skill |
|
||||
|
||||
Code blocks (fenced and inline) are always protected — their content is never transformed.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
```bash
|
||||
test -f src/text-styles.ts && echo "already applied" || echo "not yet applied"
|
||||
```
|
||||
|
||||
If `already applied`, skip to Phase 3 (Verify).
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure the upstream remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If an `upstream` remote pointing to `https://github.com/qwibitai/nanoclaw.git` is missing,
|
||||
add it:
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch upstream skill/channel-formatting
|
||||
git merge upstream/skill/channel-formatting
|
||||
```
|
||||
|
||||
If there are merge conflicts on `package-lock.json`, resolve them by accepting the incoming
|
||||
version and continuing:
|
||||
|
||||
```bash
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
```
|
||||
|
||||
For any other conflict, read the conflicted file and reconcile both sides manually.
|
||||
|
||||
This merge adds:
|
||||
|
||||
- `src/text-styles.ts` — `parseTextStyles(text, channel)` for marker substitution and
|
||||
`parseSignalStyles(text)` for Signal native rich text
|
||||
- `src/router.ts` — `formatOutbound` gains an optional `channel` parameter; when provided
|
||||
it calls `parseTextStyles` after stripping `<internal>` tags
|
||||
- `src/index.ts` — both outbound `sendMessage` paths pass `channel.name` to `formatOutbound`
|
||||
- `src/formatting.test.ts` — test coverage for both functions across all channels
|
||||
|
||||
### Validate
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/formatting.test.ts
|
||||
```
|
||||
|
||||
All 73 tests should pass and the build should be clean before continuing.
|
||||
|
||||
## Phase 3: Verify
|
||||
|
||||
### Rebuild and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
### Spot-check formatting
|
||||
|
||||
Send a message through any registered WhatsApp or Telegram chat that will trigger a
|
||||
response from Claude. Ask something that will produce formatted output, such as:
|
||||
|
||||
> Summarise the three main advantages of TypeScript using bullet points and **bold** headings.
|
||||
|
||||
Confirm that the response arrives with native bold (`*text*`) rather than raw double
|
||||
asterisks.
|
||||
|
||||
### Check logs if needed
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log
|
||||
```
|
||||
|
||||
## Signal Skill Integration
|
||||
|
||||
If you have the Signal skill installed, `src/channels/signal.ts` can import
|
||||
`parseSignalStyles` from the newly present `src/text-styles.ts`:
|
||||
|
||||
```typescript
|
||||
import { parseSignalStyles, SignalTextStyle } from '../text-styles.js';
|
||||
```
|
||||
|
||||
`parseSignalStyles` returns `{ text: string, textStyle: SignalTextStyle[] }` where
|
||||
`textStyle` is an array of `{ style, start, length }` objects suitable for the
|
||||
`signal-cli` JSON-RPC `textStyles` parameter (format: `"start:length:STYLE"`).
|
||||
|
||||
## Removal
|
||||
|
||||
```bash
|
||||
# Remove the new file
|
||||
rm src/text-styles.ts
|
||||
|
||||
# Revert router.ts to remove the channel param
|
||||
git diff upstream/main src/router.ts # review changes
|
||||
git checkout upstream/main -- src/router.ts
|
||||
|
||||
# Revert the index.ts sendMessage call sites to plain formatOutbound(rawText)
|
||||
# (edit manually or: git checkout upstream/main -- src/index.ts)
|
||||
|
||||
npm run build
|
||||
```
|
||||
@@ -80,8 +80,8 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
pnpm test
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
All tests must pass and build must be clean before proceeding.
|
||||
@@ -172,7 +172,7 @@ Expected: Both operations succeed.
|
||||
### Full integration test
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
```
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ Implementation:
|
||||
Always tell the user:
|
||||
```bash
|
||||
# Rebuild and restart
|
||||
npm run build
|
||||
pnpm run build
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
|
||||
@@ -41,7 +41,7 @@ Set `LOG_LEVEL=debug` for verbose output:
|
||||
|
||||
```bash
|
||||
# For development
|
||||
LOG_LEVEL=debug npm run dev
|
||||
LOG_LEVEL=debug pnpm run dev
|
||||
|
||||
# For launchd service (macOS), add to plist EnvironmentVariables:
|
||||
<key>LOG_LEVEL</key>
|
||||
@@ -231,7 +231,7 @@ query({
|
||||
|
||||
```bash
|
||||
# Rebuild main app
|
||||
npm run build
|
||||
pnpm run build
|
||||
|
||||
# Rebuild container (use --no-cache for clean rebuild)
|
||||
./container/build.sh
|
||||
|
||||
120
.claude/skills/init-first-agent/SKILL.md
Normal file
120
.claude/skills/init-first-agent/SKILL.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
name: init-first-agent
|
||||
description: Walk the operator through creating the first NanoClaw agent for a DM channel — resolve the operator's channel identity, wire the DM messaging group to a new agent, and trigger a welcome DM via the normal delivery path. Use after channel credentials are configured and the service is running.
|
||||
---
|
||||
|
||||
# Init First Agent
|
||||
|
||||
Stand up the first NanoClaw agent for a channel and verify end-to-end delivery by having the agent DM the operator. Everything the skill does is idempotent — rerunning is safe.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Service running.** Check: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux). If stopped, tell the user to run `/setup` first.
|
||||
- **Target channel installed.** At least one `/add-<channel>` skill has run, credentials are in `.env`, and the adapter is uncommented in `src/channels/index.ts`.
|
||||
- **Adapter connected.** Tail `logs/nanoclaw.log` — look for a recent `channel setup` / `adapter connected` line for the target channel.
|
||||
|
||||
## 1. Pick the channel
|
||||
|
||||
Read `src/channels/index.ts` to find enabled channels (uncommented imports). Cross-check `.env` for the relevant credentials.
|
||||
|
||||
AskUserQuestion: "Which channel should host the welcome DM?" with one option per enabled channel (Discord, Slack, Telegram, WhatsApp, Webex, Teams, Google Chat, Matrix, iMessage, Resend, …).
|
||||
|
||||
Record the choice as `CHANNEL` (lowercase, e.g. `discord`).
|
||||
|
||||
## 2. Ask for the operator's identity
|
||||
|
||||
Read the channel's own skill for its `## Channel Info > how-to-find-id` section (e.g. `.claude/skills/add-discord/SKILL.md`, `.claude/skills/add-telegram/SKILL.md`). Show those instructions to the user in plain text.
|
||||
|
||||
Then ask in plain text (NOT `AskUserQuestion` — these are free-form):
|
||||
|
||||
1. **Your user id on this channel** — e.g. a Discord user ID, Telegram user ID, Slack user ID. Record as `USER_HANDLE`.
|
||||
2. **Your display name** — human name, used to name the agent group (`dm-with-<normalized>`) and as the welcome-message addressee. Record as `DISPLAY_NAME`.
|
||||
3. **Agent persona name** — the assistant's display name. Default: `DISPLAY_NAME`. Record as `AGENT_NAME`.
|
||||
|
||||
## 3. Resolve the DM platform id
|
||||
|
||||
This depends on whether the channel supports cold DM via `adapter.openDM`.
|
||||
|
||||
**Channels without cold DM (direct-addressable): telegram, whatsapp, imessage, matrix, resend.** The user handle doubles as the DM chat id. Set:
|
||||
|
||||
```
|
||||
PLATFORM_ID=${CHANNEL}:${USER_HANDLE}
|
||||
```
|
||||
|
||||
Skip to step 4.
|
||||
|
||||
**Channels with cold DM (resolution-required): discord, slack, teams, webex, gchat.** The bot can DM cold at runtime via Chat SDK, but this skill runs standalone — it can't call the adapter. Two resolutions:
|
||||
|
||||
### 3a. User DMs the bot once (Discord / Slack / Teams / Webex / gChat)
|
||||
|
||||
Tell the user:
|
||||
|
||||
> Send any single message to the bot as a DM from your account on `${CHANNEL}`. The router will record the DM as a messaging group. Reply `done` here when you've sent the message.
|
||||
|
||||
Wait for the user's confirmation. Then look up the most recent DM messaging groups:
|
||||
|
||||
```bash
|
||||
sqlite3 data/v2.db "SELECT id, platform_id, name, created_at FROM messaging_groups WHERE channel_type='${CHANNEL}' AND is_group=0 ORDER BY created_at DESC LIMIT 5"
|
||||
```
|
||||
|
||||
Show the top rows to the user and confirm which `platform_id` is theirs (usually the most recent). Record as `PLATFORM_ID`. If none appeared, check `logs/nanoclaw.log` for `unknown_sender` drops — the adapter might be rejecting inbound due to connection or permission issues.
|
||||
|
||||
### 3b. Telegram pair-code path (if the user prefers not to DM first)
|
||||
|
||||
For Telegram only, there's an existing pair-code primitive. When you run this tool, take the output and extract the pairing code. Then show it to the user in plain text and ask the user to send the code in the Telegram chat to complete the pairing.
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step pair-telegram -- --intent new-agent:dm-with-<folder>
|
||||
```
|
||||
|
||||
Parse the `PAIR_TELEGRAM_ISSUED` status block for `CODE` and follow the `REMINDER_TO_ASSISTANT` line in that block. Then wait for the `PAIR_TELEGRAM` block — read `PLATFORM_ID` and `PAIRED_USER_ID` from it. telegram.ts's interceptor has already upserted the user and granted owner if none existed yet. Use `PLATFORM_ID` and `PAIRED_USER_ID` directly in step 4.
|
||||
|
||||
## 4. Run the init script
|
||||
|
||||
```bash
|
||||
npx tsx scripts/init-first-agent.ts \
|
||||
--channel "${CHANNEL}" \
|
||||
--user-id "${CHANNEL}:${USER_HANDLE}" \
|
||||
--platform-id "${PLATFORM_ID}" \
|
||||
--display-name "${DISPLAY_NAME}" \
|
||||
--agent-name "${AGENT_NAME}"
|
||||
```
|
||||
|
||||
Add `--welcome "System instruction: ..."` to override the default welcome prompt.
|
||||
|
||||
The script:
|
||||
1. Upserts the `users` row and grants `owner` role if no owner exists.
|
||||
2. Creates the `agent_groups` row and calls `initGroupFilesystem` at `groups/dm-with-<name>/`.
|
||||
3. Reuses or creates the DM `messaging_groups` row.
|
||||
4. Wires them via `messaging_group_agents` (which auto-creates the companion `agent_destinations` row).
|
||||
5. Hands the welcome message to the running service via its CLI socket (`data/cli.sock`), targeting the DM messaging group. The service routes it into the DM session, which wakes the container synchronously. If the socket isn't reachable (service down), falls back to a direct `inbound.db` write that the next host sweep picks up.
|
||||
|
||||
Show the script's output to the user.
|
||||
|
||||
## 5. Verify
|
||||
|
||||
The welcome DM is queued synchronously; the only wait is container cold-start (~60s on first launch) before the agent processes the message and the reply flows through `outbound.db` to the channel.
|
||||
|
||||
Do not tail the log or poll in a sleep loop. Ask the user in plain text:
|
||||
|
||||
> The welcome DM should arrive shortly. Let me know when you've received it (or if it doesn't arrive within two minutes).
|
||||
|
||||
Wait for the user's reply. If they confirm receipt, the skill is done.
|
||||
|
||||
If they say it didn't arrive, then diagnose using the DB directly (no waiting loops required — the message either delivered or it didn't):
|
||||
|
||||
- `sqlite3 data/v2-sessions/<agent-group-id>/sessions/<session-id>/outbound.db "SELECT id, status, created_at FROM messages_out ORDER BY created_at DESC LIMIT 5"` — check for stuck `pending` rows. Replace `<agent-group-id>` and `<session-id>` with the values from the script's output.
|
||||
- `grep -E 'Unauthorized channel destination|container.*exited|error' logs/nanoclaw.log | tail -20` — look for ACL rejections or container crashes.
|
||||
- `ls data/v2-sessions/<agent-group-id>/sessions/*/outbound.db` — confirm the session exists.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Missing required args"** — the script wants `--channel`, `--user-id`, `--platform-id`, `--display-name` at minimum. Re-check the command you assembled.
|
||||
|
||||
**No `messaging_groups` row appears after the user DMs (step 3a)** — the router silently drops messages from unknown senders under `strict` policy but still creates the `messaging_groups` row. If the row is missing entirely, the adapter isn't receiving the inbound message. Check `logs/nanoclaw.log` for adapter errors (auth, gateway disconnect, rate limit).
|
||||
|
||||
**Owner already exists** — `hasAnyOwner()` returned true, so the grant is skipped silently. That's fine; the script still creates the agent and wiring. Reassigning ownership needs a separate flow (not this skill).
|
||||
|
||||
**Wrong person got the welcome DM** — the `--platform-id` you passed is someone else's DM channel. Rerun with the correct one; the script is idempotent on user/messaging-group/agent-group but writes a new session welcome each run.
|
||||
|
||||
**Agent group name collision** — if `dm-with-<display-name>` already exists (e.g. rerunning with the same display name), the script reuses it. Pass a different `--display-name` to get a distinct folder.
|
||||
@@ -231,10 +231,10 @@ Ask them to let you know when done.
|
||||
## Phase 4: Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
If build fails, diagnose and fix. Common issue: `@onecli-sh/sdk` not installed — run `npm install` first.
|
||||
If build fails, diagnose and fix. Common issue: `@onecli-sh/sdk` not installed — run `pnpm install` first.
|
||||
|
||||
Restart the service:
|
||||
- macOS (launchd): `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
|
||||
|
||||
87
.claude/skills/manage-channels/SKILL.md
Normal file
87
.claude/skills/manage-channels/SKILL.md
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: manage-channels
|
||||
description: Wire channels to agent groups, manage isolation levels, add new channel groups. Use after adding a channel, during setup, or standalone to reconfigure.
|
||||
---
|
||||
|
||||
# Manage Channels
|
||||
|
||||
Wire messaging channels to agent groups. See `docs/isolation-model.md` for the full isolation model.
|
||||
|
||||
Privilege is a **user-level** concept, not a channel-level one (see `src/db/user-roles.ts`, `src/access.ts`). There is no "main channel" / "main group" — any user can be granted `owner` or `admin` (global or scoped to an agent group) via `grantRole()`, and messages from unknown senders are gated per-messaging-group by `unknown_sender_policy` (`strict` | `request_approval` | `public`).
|
||||
|
||||
## Assess Current State
|
||||
|
||||
Read the central DB (`data/v2.db`) — query `agent_groups`, `messaging_groups`, `messaging_group_agents`, `users`, and `user_roles` tables. Also check `.env` for channel tokens and `src/channels/index.ts` for uncommented imports.
|
||||
|
||||
Categorize channels as: **wired** (has DB entities + messaging_group_agents row), **configured but unwired** (has credentials + barrel import, no DB entities), or **not configured**.
|
||||
|
||||
If the instance has no owner yet (`SELECT COUNT(*) FROM user_roles WHERE role='owner' AND agent_group_id IS NULL` returns 0), tell the user they should run `/init-first-agent` first — it stands up the first agent group, promotes the operator to owner, and verifies delivery end-to-end by having the agent DM them. Then return here for any additional channels/groups.
|
||||
|
||||
## First Channel (No Agent Groups Exist)
|
||||
|
||||
**Delegate to `/init-first-agent`.** It handles: channel choice, operator identity lookup, DM platform id resolution (with cold-DM or pair-code fallback), agent group creation, wiring, and the welcome DM. Return here afterward for any additional channels.
|
||||
|
||||
## Wire New Channel
|
||||
|
||||
For each unwired channel:
|
||||
|
||||
1. Read its SKILL.md `## Channel Info` for terminology, how-to-find-id, typical-use, and default-isolation
|
||||
2. Ask for the platform ID using the platform's terminology
|
||||
3. Ask the isolation question (see below)
|
||||
4. Register with the appropriate flags
|
||||
|
||||
### Isolation Question
|
||||
|
||||
Present a multiple-choice with a contextual recommendation. The three options:
|
||||
|
||||
- **Same conversation** (`--session-mode "agent-shared"` + existing folder) — all messages land in one session. Recommend for webhook + chat combos (GitHub + Slack).
|
||||
- **Same agent, separate conversations** (`--session-mode "shared"` + existing folder) — shared workspace/memory, independent threads. Recommend for same user across platforms.
|
||||
- **Separate agent** (new `--folder`) — full isolation. Recommend when different people are involved.
|
||||
|
||||
Use the channel's `typical-use` and `default-isolation` fields to pick the recommendation. Offer to explain more if the user is unsure — reference `docs/isolation-model.md` for the detailed explanation.
|
||||
|
||||
### Register Command
|
||||
|
||||
```bash
|
||||
pnpm exec tsx setup/index.ts --step register -- \
|
||||
--platform-id "<id>" --name "<name>" \
|
||||
--folder "<folder>" --channel "<type>" \
|
||||
--session-mode "<shared|agent-shared|per-thread>" \
|
||||
--assistant-name "<name>"
|
||||
```
|
||||
|
||||
The `register` step creates the agent group (reusing it if the folder already exists), the messaging group, and the wiring row. `createMessagingGroupAgent` auto-creates the companion `agent_destinations` row so the agent can address the channel by name — no separate destination step needed.
|
||||
|
||||
For separate agents, also ask for a folder name and optionally a different assistant name.
|
||||
|
||||
## Add Channel Group
|
||||
|
||||
When adding another group/chat on an already-configured platform (e.g. a second Telegram group):
|
||||
|
||||
1. **Telegram:** ask the isolation question first to determine intent (`wire-to:<folder>` for an existing agent, `new-agent:<folder>` for a fresh one). Run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent <intent>`, show the CODE (follow the `REMINDER_TO_ASSISTANT` line in the `PAIR_TELEGRAM_ISSUED` block) and tell the user to post `@<botname> CODE` in the target group (or DM the bot for a private chat). Wait for the `PAIR_TELEGRAM` block. The inbound interceptor has already created the `messaging_groups` row with `unknown_sender_policy = 'strict'` and upserted the paired user — `register` only needs to add the wiring:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx setup/index.ts --step register -- \
|
||||
--platform-id "<PLATFORM_ID>" --name "<group-name>" \
|
||||
--folder "<folder>" --channel "telegram" \
|
||||
--session-mode "<shared|agent-shared|per-thread>" \
|
||||
--assistant-name "<name>"
|
||||
```
|
||||
|
||||
2. **Other channels:** read the channel's SKILL.md `## Channel Info` for terminology and how-to-find-id. Ask for the new group/chat ID, ask the isolation question, then register. No package or credential changes needed.
|
||||
|
||||
## Change Wiring
|
||||
|
||||
1. Show current wiring (agent_groups × messaging_group_agents)
|
||||
2. Ask which channel to move and to which agent group
|
||||
3. Delete the old `messaging_group_agents` entry, create a new one
|
||||
4. Note: existing sessions stay with the old agent group; new messages route to the new one. The `agent_destinations` row created for the old wiring is NOT automatically removed — if you want the old agent to stop seeing the channel as a named target, delete it from `agent_destinations` manually.
|
||||
|
||||
## Show Configuration
|
||||
|
||||
Display a readable summary showing:
|
||||
|
||||
- **Agent groups** with their wired channels (from `messaging_group_agents`)
|
||||
- **Configured-but-unwired** channels (credentials present, no DB entities)
|
||||
- **Unconfigured** channels
|
||||
- **Privileged users**: `SELECT user_id, role, agent_group_id FROM user_roles ORDER BY role='owner' DESC`
|
||||
47
.claude/skills/manage-mounts/SKILL.md
Normal file
47
.claude/skills/manage-mounts/SKILL.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: manage-mounts
|
||||
description: Configure which host directories agent containers can access. View, add, or remove mount allowlist entries. Triggers on "mounts", "mount allowlist", "agent access to directories", "container mounts".
|
||||
---
|
||||
|
||||
# Manage Mounts
|
||||
|
||||
Configure which host directories NanoClaw agent containers can access. The mount allowlist lives at `~/.config/nanoclaw/mount-allowlist.json`.
|
||||
|
||||
## Show Current Config
|
||||
|
||||
```bash
|
||||
cat ~/.config/nanoclaw/mount-allowlist.json 2>/dev/null || echo "No mount allowlist configured"
|
||||
```
|
||||
|
||||
Show the current config to the user in a readable format: which directories are allowed, whether non-main agents are read-only.
|
||||
|
||||
## Add Directories
|
||||
|
||||
Ask which directories the user wants agents to access. For each path:
|
||||
- Validate the path exists
|
||||
- Ask if it should be read-only for non-main agents (default: yes)
|
||||
|
||||
Build the JSON config and write it:
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step mounts --force -- --json '{"allowedRoots":[{"path":"/path/to/dir","readOnly":false}],"blockedPatterns":[],"nonMainReadOnly":true}'
|
||||
```
|
||||
|
||||
Use `--force` to overwrite the existing config.
|
||||
|
||||
## Remove Directories
|
||||
|
||||
Read the current config, show it, ask which entry to remove, write the updated config.
|
||||
|
||||
## Reset to Empty
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step mounts --force -- --empty
|
||||
```
|
||||
|
||||
## After Changes
|
||||
|
||||
Restart the service so containers pick up the new config:
|
||||
|
||||
- macOS: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
|
||||
- Linux: `systemctl --user restart nanoclaw`
|
||||
@@ -68,7 +68,7 @@ Source: `src/db.ts`
|
||||
Insert directly into the SQLite database. This requires groups to be registered first (Phase 1). Use the registered group's `folder` and `chat_jid`:
|
||||
|
||||
```bash
|
||||
npx tsx -e "
|
||||
pnpm exec tsx -e "
|
||||
const Database = require('better-sqlite3');
|
||||
const { CronExpressionParser } = require('cron-parser');
|
||||
const db = new Database('store/messages.db');
|
||||
|
||||
@@ -36,7 +36,7 @@ Keep it factual and terse — this is for machine recovery after compaction, not
|
||||
Run the discovery script to find and summarize the OpenClaw installation:
|
||||
|
||||
```bash
|
||||
npx tsx ${CLAUDE_SKILL_DIR}/scripts/discover-openclaw.ts
|
||||
pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/discover-openclaw.ts
|
||||
```
|
||||
|
||||
If the user specifies a custom path, pass it: `--state-dir <path>`
|
||||
@@ -106,7 +106,7 @@ The discovery script provides detected groups in the GROUPS field (format: `chan
|
||||
For each group the user wants to bring over, pre-register it:
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step register -- --jid "<nanoclaw_jid>" --name "<group_name>" --folder "<channel>_<slug>" --trigger "@<confirmed_name>" --channel <channel> --assistant-name "<confirmed_name>"
|
||||
pnpm exec tsx setup/index.ts --step register -- --jid "<nanoclaw_jid>" --name "<group_name>" --folder "<channel>_<slug>" --trigger "@<confirmed_name>" --channel <channel> --assistant-name "<confirmed_name>"
|
||||
```
|
||||
|
||||
Only pass `--assistant-name` on the first registration (it updates all CLAUDE.md templates globally).
|
||||
@@ -115,7 +115,7 @@ Folder naming: `<channel>_<name-slug>` (e.g. `whatsapp_family-chat`, `telegram_d
|
||||
|
||||
For the first/primary group, add `--is-main --no-trigger-required`. Other groups default to requiring a trigger prefix.
|
||||
|
||||
**Important:** Registration requires the database to exist. If the environment step hasn't been run yet, run it first: `npx tsx setup/index.ts --step environment`. Registration also creates the group folder under `groups/` and copies the CLAUDE.md template.
|
||||
**Important:** Registration requires the database to exist. If the environment step hasn't been run yet, run it first: `pnpm exec tsx setup/index.ts --step environment`. Registration also creates the group folder under `groups/` and copies the CLAUDE.md template.
|
||||
|
||||
Register groups from all channels — including channels NanoClaw doesn't yet support (signal, matrix, etc.). The registration stores the JID and metadata in the database, ready for when that channel is added later. Groups won't receive messages until their channel code is installed, but the registration, group folder, and CLAUDE.md will be ready.
|
||||
|
||||
@@ -295,7 +295,7 @@ For each detected plugin, present the name to the user and discuss whether to se
|
||||
|
||||
1. **If NanoClaw has a matching skill** — check the available NanoClaw skills list for an equivalent (e.g. `/add-voice-transcription` for whisper). If found, save the API key to `.env` and invoke that skill.
|
||||
|
||||
2. **If the OpenClaw plugin was an MCP server** — read its config to find the exact package name and command. Install the same MCP server (e.g. `npx -y <exact-package-from-config>`). Don't search for or guess at MCP packages — only install what was explicitly configured.
|
||||
2. **If the OpenClaw plugin was an MCP server** — read its config to find the exact package name and command. Install the same MCP server (e.g. `pnpm dlx <exact-package-from-config>`). Don't search for or guess at MCP packages — only install what was explicitly configured.
|
||||
|
||||
3. **If the OpenClaw plugin was a CLI tool** — read the config to identify the exact tool. If it's an npm package, add it to the container's Dockerfile. Add a note to the group's CLAUDE.md that the tool is available and how to invoke it.
|
||||
|
||||
@@ -324,7 +324,7 @@ Run the credential extraction script with `--write-env .env` so it writes creden
|
||||
First, run without `--write-env` to preview:
|
||||
|
||||
```bash
|
||||
npx tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts --state-dir <STATE_DIR> --channel <name>
|
||||
pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts --state-dir <STATE_DIR> --channel <name>
|
||||
```
|
||||
|
||||
Parse the status block. Key fields: HAS_CREDENTIAL, CREDENTIAL_MASKED, NANOCLAW_ENV_VAR.
|
||||
@@ -339,7 +339,7 @@ If HAS_CREDENTIAL=true: Show the masked credential (`CREDENTIAL_MASKED`). AskUse
|
||||
If using the credential:
|
||||
|
||||
```bash
|
||||
npx tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts --state-dir <STATE_DIR> --channel <name> --write-env .env
|
||||
pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts --state-dir <STATE_DIR> --channel <name> --write-env .env
|
||||
```
|
||||
|
||||
The script writes the credential directly to `.env` using the correct NanoClaw variable name (e.g. `TELEGRAM_BOT_TOKEN`). Check the status block for `WRITTEN_TO` and `WRITTEN_COUNT` to confirm.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Discover an existing OpenClaw installation and emit a structured summary.
|
||||
*
|
||||
* Usage: npx tsx .claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts [--state-dir <path>]
|
||||
* Usage: pnpm exec tsx .claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts [--state-dir <path>]
|
||||
*
|
||||
* Checks (in order): --state-dir arg, $OPENCLAW_STATE_DIR, ~/.openclaw, ~/.clawdbot
|
||||
* Parses openclaw.json (JSON5-tolerant), scans workspace for identity/memory files,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Extract a channel credential from an OpenClaw configuration and write it
|
||||
* directly to the NanoClaw .env file.
|
||||
*
|
||||
* Usage: npx tsx .claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts \
|
||||
* Usage: pnpm exec tsx .claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts \
|
||||
* --channel telegram --state-dir ~/.openclaw --write-env .env
|
||||
*
|
||||
* Handles OpenClaw SecretRef formats:
|
||||
|
||||
@@ -391,7 +391,7 @@ For behavior customizations (CLAUDE.md files): copy from the main tree. These ar
|
||||
## 2.6 Validate in worktree
|
||||
|
||||
```bash
|
||||
cd "$WORKTREE" && npm install && npm run build && npm test
|
||||
cd "$WORKTREE" && pnpm install && pnpm run build && pnpm test
|
||||
```
|
||||
|
||||
If build fails, show the error. Fix only issues caused by the migration. If unclear, ask the user.
|
||||
@@ -417,7 +417,7 @@ If testing live:
|
||||
ln -s "$PROJECT_ROOT/.env" "$WORKTREE/.env"
|
||||
```
|
||||
|
||||
3. Start from worktree: `cd "$WORKTREE" && npm run dev`
|
||||
3. Start from worktree: `cd "$WORKTREE" && pnpm run dev`
|
||||
|
||||
4. Ask the user to send a test message from their phone. Wait for them to confirm it works.
|
||||
|
||||
@@ -461,7 +461,7 @@ Do NOT use `git checkout -B` to create an intermediate branch — this caused is
|
||||
|
||||
## 2.9 Post-upgrade
|
||||
|
||||
Run `npm install && npm run build` in the main tree to confirm.
|
||||
Run `npm install && pnpm run build` in the main tree to confirm.
|
||||
|
||||
Restart the service:
|
||||
```bash
|
||||
|
||||
270
.claude/skills/new-setup/SKILL.md
Normal file
270
.claude/skills/new-setup/SKILL.md
Normal file
@@ -0,0 +1,270 @@
|
||||
---
|
||||
name: new-setup
|
||||
description: End-to-end NanoClaw setup for any user regardless of technical background — from zero to a named agent reachable on a real messaging channel, with sensible defaults and every post-verification step skippable.
|
||||
allowed-tools: Bash(bash setup.sh) Bash(bash setup/probe.sh) Bash(bash setup/install-node.sh) Bash(bash setup/install-docker.sh) Bash(bash setup/install-telegram.sh) Bash(bash setup/install-telegram.sh:*) Bash(pnpm exec tsx setup/index.ts:*) Bash(pnpm exec tsx scripts/init-first-agent.ts:*) Bash(pnpm run chat) Bash(pnpm run chat:*) Bash(open -a Docker) Bash(sudo systemctl start docker) Bash(node --version) Bash(tail:*) Bash(head:*) Bash(grep:*)
|
||||
---
|
||||
|
||||
# NanoClaw setup
|
||||
|
||||
Purpose of this skill is to take any user — technical or not — from zero to a named agent wired to a real messaging channel in the fewest steps possible.
|
||||
|
||||
The flow has two halves:
|
||||
|
||||
- **Steps 1–6 — required.** Prerequisites, credential, service start, end-to-end ping. These run straight through.
|
||||
- **Steps 7–12 — skippable.** Naming, channel wiring, QoL. Every step here is skippable: if the user says "skip", "not now", "later", or similar, move on without complaint. If they say they're done at any point, stop cleanly — don't push the remaining steps.
|
||||
|
||||
Before each step, narrate to the user in your own words what's about to happen — one short, friendly sentence, no jargon. Don't read a scripted line; use the step context below to speak naturally.
|
||||
|
||||
Each step is invoked as `pnpm exec tsx setup/index.ts --step <name>` and emits a structured status block Claude parses to decide what to do next.
|
||||
|
||||
Start with a probe: a single upfront scan that snapshots every prerequisite and dependency. The rest of the flow reads this snapshot to decide what to run, skip, or ask about — no per-step re-checking. The probe is pure bash (`setup/probe.sh`) with no external deps so it runs correctly before Node has been installed.
|
||||
|
||||
## Current state
|
||||
|
||||
!`bash setup/probe.sh`
|
||||
|
||||
## Flow
|
||||
|
||||
Parse the probe block above. For each step below, consult the named probe fields and skip, ask, or run accordingly. The probe always returns a real snapshot — there is no "node not installed" fallback; `HOST_DEPS=missing` is how you know Node/pnpm haven't been bootstrapped yet.
|
||||
|
||||
## Ordering and parallelism
|
||||
|
||||
Run steps sequentially by default: invoke the step, wait for its status block, act on the result, move to the next.
|
||||
|
||||
One permitted parallelism:
|
||||
|
||||
- **Step 2 (container image build) and step 3 (OneCLI install)** are independent — they may start together in the background.
|
||||
- **Step 4 (auth) must NOT start until step 3 has completed.** Auth writes the secret into the OneCLI vault; if OneCLI isn't installed and healthy yet, the user gets asked for a credential the system can't store. Do not open an `AskUserQuestion` for step 4 while OneCLI is still installing.
|
||||
- Step 2's image build may continue running past step 4 — the image isn't consumed until step 6 (first CLI agent). Join before step 6.
|
||||
|
||||
### 1. Node bootstrap
|
||||
|
||||
Check probe results and skip if `HOST_DEPS=ok` — Node, pnpm, `node_modules`, and `better-sqlite3`'s native binding are already in place.
|
||||
|
||||
If `HOST_DEPS=missing` and `node --version` fails (Node isn't installed at all), run `bash setup/install-node.sh` **before** `bash setup.sh` — the script handles both macOS (via `brew`) and Linux/WSL (NodeSource + apt). It's idempotent and short-circuits when node is already on PATH.
|
||||
|
||||
Then run `bash setup.sh`. If Node is already present and only `HOST_DEPS=missing`, run `bash setup.sh` directly — deps just haven't been installed yet.
|
||||
|
||||
Parse the status block:
|
||||
|
||||
- `NODE_OK=false` → Node install didn't take effect (PATH issue, keg-only formula, etc.). Investigate `logs/setup.log`, resolve, re-run.
|
||||
- `DEPS_OK=false` or `NATIVE_OK=false` → Read `logs/setup.log`, fix, re-run.
|
||||
|
||||
> **Loose command:** `bash setup.sh`. Justification: pre-Node bootstrap. Can't call the Node-based dispatcher before Node and `pnpm install` are in place.
|
||||
|
||||
### 2. Docker
|
||||
|
||||
Check probe results and skip if `DOCKER=running` AND `IMAGE_PRESENT=true`.
|
||||
|
||||
**Runtime:**
|
||||
- `DOCKER=not_found` → Docker is missing — install it so agent containers have an isolated place to run. Run `bash setup/install-docker.sh` (handles macOS via `brew --cask` and Linux via the official get.docker.com script, and adds the user to the `docker` group on Linux). On Linux the user may need to log out/in for group membership to take effect. On macOS, launch Docker afterwards with `open -a Docker`.
|
||||
- `DOCKER=installed_not_running` → Docker is installed but the daemon is down — start it.
|
||||
- macOS: `open -a Docker`
|
||||
- Linux: `sudo systemctl start docker`
|
||||
|
||||
Wait ~15s after either, then proceed.
|
||||
|
||||
> **Loose commands:** `open -a Docker`, `sudo systemctl start docker`. Justification: daemon-start is a one-liner per platform, not worth wrapping. The actual install (which had the unmatchable `curl | sh` pattern) is now inside `setup/install-docker.sh`.
|
||||
|
||||
**Image (run if `IMAGE_PRESENT=false`):** build the agent container image — takes a few minutes the first time, one-off cost.
|
||||
|
||||
`pnpm exec tsx setup/index.ts --step container -- --runtime docker`
|
||||
|
||||
### 3. OneCLI
|
||||
|
||||
Check probe results and skip if `ONECLI_STATUS=healthy`.
|
||||
|
||||
OneCLI is the local vault that holds API keys and only releases them to agents when they need them.
|
||||
|
||||
`pnpm exec tsx setup/index.ts --step onecli`
|
||||
|
||||
### 4. Anthropic credential
|
||||
|
||||
Check probe results and skip if `ANTHROPIC_SECRET=true`.
|
||||
|
||||
The credential never travels through chat — the user generates it, registers it with OneCLI themselves, and the skill verifies.
|
||||
|
||||
**4a. Pick the source.** `AskUserQuestion`:
|
||||
|
||||
1. **Claude subscription (Pro/Max)** — "Generate a token via `claude setup-token` in another terminal."
|
||||
2. **Anthropic API key** — "Use a pay-per-use key from console.anthropic.com/settings/keys."
|
||||
|
||||
**4b. Wait for the user to obtain the credential.** For subscription, have them run `claude setup-token` in another terminal. For API key, point them to the console URL above. Either way, they keep the token — just confirm when they have it.
|
||||
|
||||
**4c. Pick the registration path.** `AskUserQuestion` — substitute `${ONECLI_URL}` from the probe (or `.env`):
|
||||
|
||||
1. **Dashboard** — "Open ${ONECLI_URL} in a browser; add a secret of type `anthropic`, value = the token, host-pattern `api.anthropic.com`."
|
||||
2. **CLI** — "Run in another terminal: `onecli secrets create --name Anthropic --type anthropic --value YOUR_TOKEN --host-pattern api.anthropic.com`"
|
||||
|
||||
Wait for the user's confirmation. If their reply happens to include a token (starts with `sk-ant-`), register it for them: `pnpm exec tsx setup/index.ts --step auth -- --create --value <TOKEN>`.
|
||||
|
||||
**4d. Verify.**
|
||||
|
||||
`pnpm exec tsx setup/index.ts --step auth -- --check`
|
||||
|
||||
If `ANTHROPIC_OK=false`, the secret isn't there yet — ask them to retry, then re-check.
|
||||
|
||||
### 5. Service
|
||||
|
||||
Check probe results and skip if `SERVICE_STATUS=running`.
|
||||
|
||||
Start the NanoClaw background service — it relays messages between the user and the agent.
|
||||
|
||||
`pnpm exec tsx setup/index.ts --step service`
|
||||
|
||||
### 6. Wire a scratch CLI agent and verify end-to-end
|
||||
|
||||
**Do not narrate this step.** No "creating your first agent…", no "sending a ping…" chatter. The user's experience here is: they finished the last visible step (service), then a single success line appears. Wiring is an implementation detail at this point, not a user-facing milestone.
|
||||
|
||||
If step 2's container build is still running in the background, join it here before proceeding — the agent needs the image.
|
||||
|
||||
Use `INFERRED_DISPLAY_NAME` from the probe silently. **Do not ask the user.** The CLI agent at this stage is a scratch agent whose only purpose is to verify the end-to-end pipeline (host → container → agent → back). The user's real name capture happens in step 7.
|
||||
|
||||
Run wiring and ping back-to-back, silently:
|
||||
|
||||
```
|
||||
pnpm exec tsx setup/index.ts --step cli-agent -- --display-name "<INFERRED_DISPLAY_NAME>"
|
||||
pnpm run chat ping
|
||||
```
|
||||
|
||||
First container spin-up takes ~60s. When the agent's reply arrives, emit exactly one line to the user:
|
||||
|
||||
> Test Agent success, proceeding with setup
|
||||
|
||||
If `pnpm run chat ping` times out or errors, tail `logs/nanoclaw.log`, diagnose, and fix — don't surface a half-success.
|
||||
|
||||
> **Loose command:** `pnpm run chat ping`. Justification: this is the same command the user will keep using after setup, so verification and the real channel are one and the same.
|
||||
|
||||
### 7. What should the agent call you?
|
||||
|
||||
Plain-prose ask (do **not** use `AskUserQuestion`):
|
||||
|
||||
> What should your agent call you? (Default: `<INFERRED_DISPLAY_NAME>`)
|
||||
|
||||
Capture the answer into a local variable `OPERATOR_NAME`. **Don't persist yet** — this value is consumed by step 10's channel wiring. If the user skips or confirms the default, set `OPERATOR_NAME = INFERRED_DISPLAY_NAME`.
|
||||
|
||||
### 8. What's your agent's name?
|
||||
|
||||
Plain-prose ask:
|
||||
|
||||
> What would you like to call your agent? (Default: `<OPERATOR_NAME>`)
|
||||
|
||||
Capture as `AGENT_NAME`. If skipped, set `AGENT_NAME = OPERATOR_NAME`. Nothing persisted yet.
|
||||
|
||||
### 9. Timezone
|
||||
|
||||
Run `pnpm exec tsx setup/index.ts --step timezone` and parse the status block.
|
||||
|
||||
- **RESOLVED_TZ is `UTC` or `Etc/UTC`** — before leaving UTC in `.env`, confirm with `AskUserQuestion`:
|
||||
|
||||
- **Question**: "Your system reports UTC as the timezone. Is that right, or are you somewhere else?"
|
||||
- **Header**: "Timezone"
|
||||
- **Options**:
|
||||
1. `Keep UTC` — "Leave timezone as UTC."
|
||||
2. `I'm somewhere else` — "I'll name the IANA zone (e.g. `America/New_York`, `Europe/London`, `Asia/Tokyo`) via Other."
|
||||
|
||||
If they pick "I'm somewhere else" (or type an IANA zone via Other), re-run `pnpm exec tsx setup/index.ts --step timezone -- --tz <answer>` to overwrite `.env`. If they keep UTC or skip, leave UTC in place.
|
||||
|
||||
- **NEEDS_USER_INPUT=true** — autodetection failed. Use `AskUserQuestion` with the same two options above (reword the question to "Autodetection failed — what timezone are you in?"), then re-run `pnpm exec tsx setup/index.ts --step timezone -- --tz <answer>` if they supply one. If they skip, move on.
|
||||
|
||||
- Otherwise — timezone is already set; move on.
|
||||
|
||||
### 10. Pick a messaging channel
|
||||
|
||||
Print the list as a numbered plain-prose list (too many options for `AskUserQuestion`, which caps at 4). The user replies with a number or channel name. Preserve the numbering exactly:
|
||||
|
||||
> Which messaging channel should I wire your agent to?
|
||||
>
|
||||
> 1. **WhatsApp (native)** — `/add-whatsapp`
|
||||
> 2. **WhatsApp Cloud (Meta official)** — `/add-whatsapp-cloud`
|
||||
> 3. **Telegram** — `/add-telegram`
|
||||
> 4. **Slack** — `/add-slack`
|
||||
> 5. **Discord** — `/add-discord`
|
||||
> 6. **iMessage** — `/add-imessage`
|
||||
> 7. **Teams** — `/add-teams`
|
||||
> 8. **Matrix** — `/add-matrix`
|
||||
> 9. **Google Chat** — `/add-gchat`
|
||||
> 10. **Linear** — `/add-linear`
|
||||
> 11. **GitHub** — `/add-github`
|
||||
> 12. **Webex** — `/add-webex`
|
||||
> 13. **Resend (email)** — `/add-resend`
|
||||
> 14. **Emacs** — `/add-emacs`
|
||||
> 15. **WeChat** — `/add-wechat`
|
||||
>
|
||||
> Or say "skip" to leave this for later.
|
||||
|
||||
When the user picks one:
|
||||
|
||||
1. **Install the adapter.** For **Telegram**, run `bash setup/install-telegram.sh` directly — it bundles the preflight + fetch + copy + register + `pnpm install` + build from `/add-telegram` into one idempotent call. Then handle Telegram credentials inline (below) — **do not** invoke `/add-telegram` afterward; its Credentials section would generate an unapprovable `grep && sed && rm` to write `.env`. For every other channel, invoke the matching `/add-<channel>` skill via the Skill tool; it copies the adapter source in from the `channels` branch, registers it, installs the pinned npm package, and handles credentials. Some channels also run a pairing step as part of their flow.
|
||||
|
||||
**Telegram credentials (inline):**
|
||||
- Walk the user through BotFather: `/newbot` → pick name + username ending in `bot` → copy the token.
|
||||
- Remind them: in `@BotFather` → `/mybots` → their bot → Bot Settings → Group Privacy → **Turn off** (only needed if the bot will live in groups; DM-only can skip).
|
||||
- Persist the token and sync it to the container mount with the generic setter:
|
||||
|
||||
```
|
||||
pnpm exec tsx setup/index.ts --step set-env -- \
|
||||
--key TELEGRAM_BOT_TOKEN --value "<token>" --sync-container
|
||||
```
|
||||
|
||||
2. **Capture platform IDs.** After the `/add-<channel>` skill finishes (or after inline credentials for Telegram), you need two values: the operator's user-id on that platform, and the chat/channel platform-id. Each channel surfaces these differently — consult the **Channel Info** section at the bottom of that skill's `SKILL.md` for the exact path. For Telegram, run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent <main|wire-to:folder|new-agent:folder>` directly and follow its `PAIR_TELEGRAM_ISSUED`/`PAIR_TELEGRAM STATUS=success` blocks — `PLATFORM_ID` and `ADMIN_USER_ID` land in the success block.
|
||||
3. **Wire the agent.** Run `init-first-agent.ts` in DM mode:
|
||||
|
||||
```
|
||||
pnpm exec tsx scripts/init-first-agent.ts \
|
||||
--channel <channel> \
|
||||
--user-id "<platform-user-id>" \
|
||||
--platform-id "<platform-chat-id>" \
|
||||
--display-name "<OPERATOR_NAME>" \
|
||||
--agent-name "<AGENT_NAME>"
|
||||
```
|
||||
|
||||
4. **Announce.** On success, emit the encouragement line verbatim:
|
||||
|
||||
> Your agent is now available on {channel-name}, you can already start chatting — But I encourage you to continue and finish this setup, we're almost done!
|
||||
|
||||
Substitute `{channel-name}` with the friendly name (e.g. "Telegram", "WhatsApp", "Slack").
|
||||
|
||||
If the user skipped, move on to step 11.
|
||||
|
||||
### 11. Host directory access
|
||||
|
||||
By default, agent containers can only touch their own workspace. If the user wants the agent to read or write files in specific host directories, those paths need to go on the mount allowlist.
|
||||
|
||||
Use `AskUserQuestion`:
|
||||
|
||||
- **Question**: "Want your agent to read or write files in any host directories (e.g. a code project, `~/Documents`)?"
|
||||
- **Header**: "Host mounts"
|
||||
- **Options**:
|
||||
1. `Keep isolated` — "Agent only touches its own workspace (Recommended)."
|
||||
2. `Add host paths` — "I'll name the directories to allowlist via Other."
|
||||
|
||||
If they pick "Add host paths" (or name paths via Other), invoke `/manage-mounts` via the Skill tool to add them. If they keep it isolated or skip, move on.
|
||||
|
||||
### 12. Quality of life
|
||||
|
||||
Optional polish. Print the list; the user may pick zero, one, or several — invoke each chosen skill in sequence:
|
||||
|
||||
> Want to add any of these? Pick any that sound useful — or skip:
|
||||
>
|
||||
> - `/add-dashboard` — browser dashboard showing agent activity
|
||||
> - `/add-compact` — `/compact` slash command for managing long sessions
|
||||
> - `/add-karpathy-llm-wiki` — persistent knowledge-base memory for the agent
|
||||
|
||||
If the probe reports `PLATFORM=darwin`, also offer:
|
||||
|
||||
> - `/add-macos-statusbar` — macOS menu bar indicator with Start/Stop/Restart controls
|
||||
|
||||
Do **not** list `/add-macos-statusbar` on Linux. If the user skips everything, just move on.
|
||||
|
||||
### 13. Done
|
||||
|
||||
Short wrap-up:
|
||||
|
||||
> Setup complete. You can chat with your agent on {channel-name} — or via CLI with `pnpm run chat <message>`.
|
||||
|
||||
Substitute `{channel-name}` with whatever was wired in step 10. If step 10 was skipped, drop the "on {channel-name} — or" clause entirely so the line just mentions the CLI form.
|
||||
|
||||
## If anything fails
|
||||
|
||||
Any step that reports `STATUS: failed` in its status block: read `logs/setup.log` (or `logs/nanoclaw.log` for runtime failures), diagnose, fix the underlying cause, re-run the same `--step`. Don't bypass errors to keep moving.
|
||||
@@ -5,50 +5,41 @@ description: Run initial NanoClaw setup. Use when user wants to install dependen
|
||||
|
||||
# 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`.
|
||||
Welcome the user to NanoClaw. Introduce yourself — you'll be walking them through the entire setup process step by step, from installing dependencies to getting their first message through. Keep it warm and brief (2-3 sentences).
|
||||
|
||||
**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.
|
||||
Then explain that setup involves running many shell commands (installing packages, building containers, starting services), and recommend pre-approving the standard setup commands so they don't have to confirm each one individually.
|
||||
|
||||
**UX Note:** Use `AskUserQuestion` for multiple-choice questions only (e.g. "Docker or Apple Container?", "which channels?"). Do NOT use it when free-text input is needed (e.g. phone numbers, tokens, paths) — just ask the question in plain text and wait for the user's reply.
|
||||
Use `AskUserQuestion` with these options:
|
||||
|
||||
## 0. Git & Fork Setup
|
||||
1. **Pre-approve (recommended)** — description: "Pre-approve standard setup commands so you don't have to confirm each one. You can review the list first if you'd like."
|
||||
2. **No thanks** — description: "I'll approve each command individually as it comes up."
|
||||
3. **Show me the list first** — description: "Show me exactly which commands will be pre-approved before I decide."
|
||||
|
||||
Check the git remote configuration to ensure the user has a fork and upstream is configured.
|
||||
If they pick option 1: read `.claude/skills/setup/setup-permissions.json`, then read the project settings file at `.claude/settings.json` (create it if it doesn't exist with `{}`), and directly edit it to add/merge the permissions into the `permissions.allow` array. Do NOT use the `update-config` skill.
|
||||
|
||||
Run:
|
||||
- `git remote -v`
|
||||
If they pick option 3: read and display `.claude/skills/setup/setup-permissions.json`, then re-ask with just options 1 and 2.
|
||||
|
||||
**Case A — `origin` points to `qwibitai/nanoclaw` (user cloned directly):**
|
||||
If they decline, continue — they'll approve commands individually.
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
**Internal guidance (do not show to user):**
|
||||
|
||||
- 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`.
|
||||
- **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.
|
||||
- **UX Note:** Use `AskUserQuestion` for multiple-choice questions only (e.g. "which credential method?"). Do NOT use it when free-text input is needed (e.g. phone numbers, tokens, paths) — just ask the question in plain text and wait for the user's reply.
|
||||
- **Timeouts:** Use 5m timeouts for install and build steps.
|
||||
- **Waiting on user:** When the user needs to do something (change a setting, get a token, open a browser, etc.), stop and wait. Give clear instructions, then say "Let me know when done or if you need help." Do NOT continue to the next step. If they ask for help, give more detail, ask where they got stuck, and try to assist.
|
||||
|
||||
## 0. Git Upstream
|
||||
|
||||
Ensure `upstream` remote points to `qwibitai/nanoclaw`. If missing, add it silently:
|
||||
|
||||
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
|
||||
git remote -v
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git 2>/dev/null || true
|
||||
```
|
||||
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)
|
||||
|
||||
@@ -64,21 +55,15 @@ Run `bash setup.sh` and parse the status block.
|
||||
|
||||
## 2. Check Environment
|
||||
|
||||
Run `npx tsx setup/index.ts --step environment` and parse the status block.
|
||||
Run `pnpm exec 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
|
||||
- Record APPLE_CONTAINER and DOCKER values for step 3
|
||||
- Record DOCKER value for step 3
|
||||
|
||||
### OpenClaw Migration Detection
|
||||
|
||||
Check for an existing OpenClaw installation:
|
||||
|
||||
```bash
|
||||
ls -d ~/.openclaw 2>/dev/null || ls -d ~/.clawdbot 2>/dev/null
|
||||
```
|
||||
|
||||
If a directory is found, AskUserQuestion:
|
||||
If OPENCLAW_PATH is not `none` from the environment check above, AskUserQuestion:
|
||||
|
||||
1. **Migrate now** — "Import identity, credentials, and settings from OpenClaw before continuing setup."
|
||||
2. **Fresh start** — "Skip migration and set up NanoClaw from scratch."
|
||||
@@ -88,61 +73,51 @@ If "Migrate now": invoke `/migrate-from-openclaw`, then return here and continue
|
||||
|
||||
## 2a. Timezone
|
||||
|
||||
Run `npx tsx setup/index.ts --step timezone` and parse the status block.
|
||||
Run `pnpm exec tsx setup/index.ts --step timezone` and parse the status block.
|
||||
|
||||
- If NEEDS_USER_INPUT=true → The system timezone could not be autodetected (e.g. POSIX-style TZ like `IST-2`). AskUserQuestion: "What is your timezone?" with common options (America/New_York, Europe/London, Asia/Jerusalem, Asia/Tokyo) and an "Other" escape. Then re-run: `npx tsx setup/index.ts --step timezone -- --tz <their-answer>`.
|
||||
- If NEEDS_USER_INPUT=true → The system timezone could not be autodetected (e.g. POSIX-style TZ like `IST-2`). AskUserQuestion: "What is your timezone?" with common options (America/New_York, Europe/London, Asia/Jerusalem, Asia/Tokyo) and an "Other" escape. Then re-run: `pnpm exec tsx setup/index.ts --step timezone -- --tz <their-answer>`.
|
||||
- If STATUS=success and RESOLVED_TZ is `UTC` or `Etc/UTC` → confirm with the user: "Your system timezone is UTC — is that correct, or are you on a remote server?" If wrong, ask for their actual timezone and re-run with `--tz`.
|
||||
- If STATUS=success → Timezone is configured. Note RESOLVED_TZ for reference.
|
||||
|
||||
## 3. Container Runtime
|
||||
## 3. Container Runtime (Docker)
|
||||
|
||||
### 3a. Choose runtime
|
||||
### 3a. Install Docker
|
||||
|
||||
Check the preflight results for `APPLE_CONTAINER` and `DOCKER`, and the PLATFORM from step 1.
|
||||
|
||||
- PLATFORM=linux → Docker (only option)
|
||||
- PLATFORM=macos + APPLE_CONTAINER=installed → AskUserQuestion with two options:
|
||||
1. **Docker (recommended)** — description: "Cross-platform, better credential management, well-tested."
|
||||
2. **Apple Container (experimental)** — description: "Native macOS runtime. Requires advanced setup."
|
||||
If Apple Container, run `/convert-to-apple-container` now, then skip to 3c.
|
||||
- PLATFORM=macos + APPLE_CONTAINER=not_found → Docker
|
||||
|
||||
### 3a-docker. Install Docker
|
||||
|
||||
- DOCKER=running → continue to 4b
|
||||
- DOCKER=running → continue to step 4
|
||||
- DOCKER=installed_not_running → start Docker: `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux). Wait 15s, re-check with `docker info`.
|
||||
- DOCKER=not_found → Use `AskUserQuestion: Docker is required for running agents. Would you like me to install it?` If confirmed:
|
||||
- macOS: install via `brew install --cask docker`, then `open -a Docker` and wait for it to start. If brew not available, direct to Docker Desktop download at https://docker.com/products/docker-desktop
|
||||
- Linux: install with `curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER`. Note: user may need to log out/in for group membership.
|
||||
|
||||
### 3b. Apple Container conversion gate (if needed)
|
||||
### 3b. CJK fonts
|
||||
|
||||
**If the chosen runtime is Apple Container**, you MUST check whether the source code has already been converted from Docker to Apple Container. Do NOT skip this step. Run:
|
||||
Agent containers skip CJK fonts by default (~200MB saved). Without them, Chromium-rendered screenshots and PDFs show tofu for Chinese/Japanese/Korean.
|
||||
|
||||
- **User writing to you in Chinese, Japanese, or Korean** → enable without asking. Mention it briefly.
|
||||
- **Resolved timezone from step 2a is a CJK region** (`Asia/Tokyo`, `Asia/Shanghai`, `Asia/Hong_Kong`, `Asia/Taipei`, `Asia/Seoul`) or other signal short of active CJK use → ask: "Enable CJK fonts? Adds ~200MB, lets the agent render CJK in screenshots and PDFs."
|
||||
- **Otherwise** → skip.
|
||||
|
||||
To enable, write `INSTALL_CJK_FONTS=true` to `.env`:
|
||||
|
||||
```bash
|
||||
grep -q "CONTAINER_RUNTIME_BIN = 'container'" src/container-runtime.ts && echo "ALREADY_CONVERTED" || echo "NEEDS_CONVERSION"
|
||||
grep -q '^INSTALL_CJK_FONTS=' .env && sed -i.bak 's/^INSTALL_CJK_FONTS=.*/INSTALL_CJK_FONTS=true/' .env && rm -f .env.bak || echo 'INSTALL_CJK_FONTS=true' >> .env
|
||||
```
|
||||
|
||||
**If NEEDS_CONVERSION**, the source code still uses Docker as the runtime. You MUST run the `/convert-to-apple-container` skill NOW, before proceeding to the build step.
|
||||
|
||||
**If ALREADY_CONVERTED**, the code already uses Apple Container. Continue to 3c.
|
||||
|
||||
**If the chosen runtime is Docker**, no conversion is needed. Continue to 3c.
|
||||
The next step's build picks it up automatically.
|
||||
|
||||
### 3c. Build and test
|
||||
|
||||
Run `npx tsx setup/index.ts --step container -- --runtime <chosen>` and parse the status block.
|
||||
Run `pnpm exec tsx setup/index.ts --step container -- --runtime docker` and parse the status block.
|
||||
|
||||
**If BUILD_OK=false:** Read `logs/setup.log` tail for the build error.
|
||||
- Cache issue (stale layers): `docker builder prune -f` (Docker) or `container builder stop && container builder rm && container builder start` (Apple Container). Retry.
|
||||
- Cache issue (stale layers): `docker builder prune -f`. Retry.
|
||||
- Dockerfile syntax or missing files: diagnose from the log and fix, then retry.
|
||||
|
||||
**If TEST_OK=false but BUILD_OK=true:** The image built but won't run. Check logs — common cause is runtime not fully started. Wait a moment and retry the test.
|
||||
|
||||
## 4. Credential System
|
||||
|
||||
The credential system depends on the container runtime chosen in step 3.
|
||||
|
||||
### 4a. Docker → OneCLI
|
||||
### 4a. OneCLI
|
||||
|
||||
Install OneCLI and its CLI tool:
|
||||
|
||||
@@ -214,69 +189,65 @@ Ask them to let you know when done.
|
||||
|
||||
**After user confirms:** verify with `onecli secrets list` that an Anthropic secret exists. If not, ask again.
|
||||
|
||||
### 4b. Apple Container → Native Credential Proxy
|
||||
|
||||
Apple Container is not compatible with OneCLI. The credential proxy code is already included in the apple-container branch — do NOT invoke `/use-native-credential-proxy` (it would conflict with already-applied code).
|
||||
|
||||
Instead, just configure the credentials in `.env`:
|
||||
|
||||
AskUserQuestion: Do you want to use your **Claude subscription** (Pro/Max) or an **Anthropic API key**?
|
||||
|
||||
1. **Claude subscription (Pro/Max)** — description: "Uses your existing Claude Pro or Max subscription. Run `claude setup-token` in another terminal to get your token."
|
||||
2. **Anthropic API key** — description: "Pay-per-use API key from console.anthropic.com."
|
||||
|
||||
For subscription: tell the user to run `claude setup-token` in another terminal. Stop and wait for the user to confirm they have completed this step successfully before proceeding.
|
||||
|
||||
Once confirmed, add the token to `.env`:
|
||||
```bash
|
||||
echo 'CLAUDE_CODE_OAUTH_TOKEN=<their-token>' >> .env
|
||||
```
|
||||
|
||||
For API key: add to `.env`:
|
||||
```bash
|
||||
echo 'ANTHROPIC_API_KEY=<their-key>' >> .env
|
||||
```
|
||||
|
||||
Verify the proxy starts: `npm run dev` should show "Credential proxy listening" in the logs.
|
||||
|
||||
## 5. 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)
|
||||
Show the full list of available channels in plain text (do NOT use AskUserQuestion — it limits to 4 options). Ask which one they want to start with. They can add more later with `/customize`.
|
||||
|
||||
**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.
|
||||
Channels where the agent gets its own identity (name and avatar) are marked as recommended.
|
||||
|
||||
For each selected channel, invoke its skill:
|
||||
1. Discord *(recommended — agent gets own identity)*
|
||||
2. Slack *(recommended — agent gets own identity)*
|
||||
3. Telegram *(recommended — agent gets own identity)*
|
||||
4. Microsoft Teams *(recommended — agent gets own identity)*
|
||||
5. Webex *(recommended — agent gets own identity)*
|
||||
6. WhatsApp
|
||||
7. WhatsApp Cloud API
|
||||
8. iMessage
|
||||
9. GitHub
|
||||
10. Linear
|
||||
11. Google Chat
|
||||
12. Resend (email)
|
||||
13. Matrix
|
||||
|
||||
**Delegate to the selected channel's skill.** Each channel skill handles its own package installation, authentication, registration, and configuration.
|
||||
|
||||
Invoke the matching skill:
|
||||
|
||||
- **WhatsApp:** Invoke `/add-whatsapp`
|
||||
- **Telegram:** Invoke `/add-telegram`
|
||||
- **Slack:** Invoke `/add-slack`
|
||||
- **Discord:** Invoke `/add-discord`
|
||||
- **Slack:** Invoke `/add-slack`
|
||||
- **Telegram:** Invoke `/add-telegram`
|
||||
- **GitHub:** Invoke `/add-github`
|
||||
- **Linear:** Invoke `/add-linear`
|
||||
- **Microsoft Teams:** Invoke `/add-teams`
|
||||
- **Google Chat:** Invoke `/add-gchat`
|
||||
- **WhatsApp Cloud API:** Invoke `/add-whatsapp-cloud`
|
||||
- **WhatsApp Baileys:** Invoke `/add-whatsapp`
|
||||
- **Resend:** Invoke `/add-resend`
|
||||
- **Matrix:** Invoke `/add-matrix`
|
||||
- **Webex:** Invoke `/add-webex`
|
||||
- **iMessage:** Invoke `/add-imessage`
|
||||
|
||||
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
|
||||
The skill will:
|
||||
1. Install the Chat SDK adapter package
|
||||
2. Uncomment the channel import in `src/channels/index.ts`
|
||||
3. Collect credentials/tokens and write to `.env`
|
||||
4. Build and verify
|
||||
|
||||
**After all channel skills complete**, install dependencies and rebuild — channel merges may introduce new packages:
|
||||
**After the channel skill completes**, install dependencies and rebuild — channel merges may introduce new packages:
|
||||
|
||||
```bash
|
||||
npm install && npm run build
|
||||
pnpm install && pnpm run build
|
||||
```
|
||||
|
||||
If the build fails, read the error output and fix it (usually a missing dependency). Then continue to step 6.
|
||||
If the build fails, read the error output and fix it (usually a missing dependency). Then continue to step 5a.
|
||||
|
||||
## 6. Mount Allowlist
|
||||
|
||||
AskUserQuestion: Agent access to external directories?
|
||||
Set empty mount allowlist (agents only access their own workspace). Users can configure mounts later with `/manage-mounts`.
|
||||
|
||||
**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}'`
|
||||
```bash
|
||||
pnpm exec tsx setup/index.ts --step mounts -- --empty
|
||||
```
|
||||
|
||||
## 7. Start Service
|
||||
|
||||
@@ -284,7 +255,7 @@ 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.
|
||||
Run `pnpm exec tsx setup/index.ts --step service` and parse the status block.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -308,27 +279,47 @@ Replace `USERNAME` with the actual username (from `whoami`). Run the two `sudo`
|
||||
- Linux: check `systemctl --user status nanoclaw`.
|
||||
- Re-run the service step after fixing.
|
||||
|
||||
## 7a. Wire Channels to Agent Groups
|
||||
|
||||
The service is now running, so polling-based adapters (Telegram) can observe inbound messages — required for pairing.
|
||||
|
||||
Invoke `/manage-channels` to wire the installed channels to agent groups. This step:
|
||||
1. Creates the agent group(s) and assigns a name to the assistant
|
||||
2. Resolves each channel's platform-specific ID (Telegram via pairing code; other channels via the platform's own ID lookup)
|
||||
3. Decides the isolation level — whether channels share an agent, session, or are fully separate
|
||||
|
||||
The `/manage-channels` skill reads each channel's `## Channel Info` section from its SKILL.md for platform-specific guidance (terminology, how to find IDs, recommended isolation).
|
||||
|
||||
**This step is required.** Without it, channels are installed but not wired — messages will be silently dropped because the router has no agent group to route to.
|
||||
|
||||
## 7b. Dashboard & Web Applications
|
||||
|
||||
AskUserQuestion: Do you want to create a dashboard and build web applications?
|
||||
|
||||
1. **Yes (recommended)** — description: "Get a NanoClaw dashboard to monitor your agents and build custom websites however you want. Deploys to Vercel."
|
||||
2. **Not now** — description: "You can add this later with `/add-vercel`."
|
||||
|
||||
If yes: invoke `/add-vercel`.
|
||||
|
||||
## 8. Verify
|
||||
|
||||
Run `npx tsx setup/index.ts --step verify` and parse the status block.
|
||||
Run `pnpm exec tsx setup/index.ts --step verify` and parse the status block.
|
||||
|
||||
**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=stopped → `pnpm 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 7
|
||||
- CREDENTIALS=missing → re-run step 4 (Docker: check `onecli secrets list`; Apple Container: check `.env` for credentials)
|
||||
- CREDENTIALS=missing → re-run step 4 (check `onecli secrets list`)
|
||||
- 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 5
|
||||
- MOUNT_ALLOWLIST=missing → `npx tsx setup/index.ts --step mounts -- --empty`
|
||||
|
||||
- REGISTERED_GROUPS=0 → re-invoke `/manage-channels` from step 7a
|
||||
Tell user to test: send a message in their registered chat. Show: `tail -f logs/nanoclaw.log`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), credential system not running (Docker: check `curl ${ONECLI_URL}/api/health`; Apple Container: check `.env` credentials), missing channel credentials (re-invoke channel skill).
|
||||
**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), credential system not running (check `curl ${ONECLI_URL}/api/health`), missing channel credentials (re-invoke channel skill).
|
||||
|
||||
**Container agent fails ("Claude Code process exited with code 1"):** Ensure the container runtime is running — `open -a Docker` (macOS Docker), `container system start` (Apple Container), or `sudo systemctl start docker` (Linux). Check container logs in `groups/main/logs/container-*.log`.
|
||||
**Container agent fails ("Claude Code process exited with code 1"):** Ensure Docker is running — `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux). Check container logs in `groups/main/logs/container-*.log`.
|
||||
|
||||
**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`.
|
||||
**No response to messages:** Check trigger pattern. Main channel doesn't need prefix. Check DB: `pnpm exec tsx setup/index.ts --step verify`. Check `logs/nanoclaw.log`.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -339,3 +330,20 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/
|
||||
|
||||
1. Use the Read tool to read `.claude/skills/setup/diagnostics.md`.
|
||||
2. Follow every step in that file before completing setup.
|
||||
|
||||
## 10. Fork Setup
|
||||
|
||||
Only run this after the user has confirmed 2-way messaging works.
|
||||
|
||||
Check `git remote -v`. If `origin` points to `qwibitai/nanoclaw` (not a fork), ask in plain text:
|
||||
|
||||
> We recommend forking NanoClaw so you can push your customizations and pull updates easily. Would you like to set up a fork now?
|
||||
|
||||
If yes: instruct the user to fork `qwibitai/nanoclaw` on GitHub (they need to do this in their browser), then ask 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
|
||||
```
|
||||
|
||||
If no: skip — upstream is already configured from step 0.
|
||||
|
||||
34
.claude/skills/setup/setup-permissions.json
Normal file
34
.claude/skills/setup/setup-permissions.json
Normal file
@@ -0,0 +1,34 @@
|
||||
[
|
||||
"Bash(bash setup.sh*)",
|
||||
"Bash(git remote *)",
|
||||
"Bash(npx tsx setup/index.ts*)",
|
||||
"Bash(npx tsx scripts/init-first-agent.ts*)",
|
||||
"Bash(npm install @chat-adapter/*)",
|
||||
"Bash(npm install chat-adapter-imessage*)",
|
||||
"Bash(npm install @bitbasti/chat-adapter-webex*)",
|
||||
"Bash(npm install @resend/chat-sdk-adapter*)",
|
||||
"Bash(npm install @whiskeysockets/baileys*)",
|
||||
"Bash(npm install @beeper/chat-adapter-matrix*)",
|
||||
"Bash(npm install @nanoco/nanoclaw-dashboard*)",
|
||||
"Bash(npm ci*)",
|
||||
"Bash(npm run build*)",
|
||||
"Bash(curl -fsSL onecli.sh*)",
|
||||
"Bash(onecli *)",
|
||||
"Bash(grep -q *)",
|
||||
"Bash(echo *>> .env)",
|
||||
"Bash(ls *)",
|
||||
"Bash(cat ~/.config/nanoclaw/*)",
|
||||
"Bash(tail *logs/*)",
|
||||
"Bash(launchctl *nanoclaw*)",
|
||||
"Bash(sqlite3 data/*)",
|
||||
"Bash(docker info*)",
|
||||
"Bash(docker logs *)",
|
||||
"Bash(mkdir -p *)",
|
||||
"Bash(cp .env *)",
|
||||
"Bash(rsync -a .claude/skills/*)",
|
||||
"Bash(head *)",
|
||||
"Bash(xattr *)",
|
||||
"Bash(find ~/.npm *)",
|
||||
"Bash(which onecli*)",
|
||||
"Bash(./container/build.sh*)"
|
||||
]
|
||||
@@ -30,7 +30,7 @@ Run `/update-nanoclaw` in Claude Code.
|
||||
|
||||
**Conflict resolution**: opens only conflicted files, resolves the conflict markers, keeps your local customizations intact.
|
||||
|
||||
**Validation**: runs `npm run build` and `npm test`.
|
||||
**Validation**: runs `pnpm run build` and `pnpm 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.
|
||||
|
||||
@@ -109,7 +109,7 @@ Show file-level impact from upstream:
|
||||
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
|
||||
- **Build/config** (`package.json`, `pnpm-lock.yaml`, `tsconfig*.json`, `container/`, `launchd/`): review needed
|
||||
- **Other**: docs, tests, misc
|
||||
|
||||
**Large drift check:** If the upstream commit count and age suggest the user has a lot of catching up to do, mention that `/migrate-nanoclaw` might be a better fit — it extracts customizations and reapplies them on clean upstream instead of merging. Offer it as an option but don't push.
|
||||
@@ -175,8 +175,8 @@ If it gets messy (more than 3 rounds of conflicts):
|
||||
|
||||
# Step 5: Validation
|
||||
Run:
|
||||
- `npm run build`
|
||||
- `npm test` (do not fail the flow if tests are not configured)
|
||||
- `pnpm run build`
|
||||
- `pnpm test` (do not fail the flow if tests are not configured)
|
||||
|
||||
If build fails:
|
||||
- Show the error.
|
||||
@@ -234,7 +234,7 @@ Tell the user:
|
||||
- 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`
|
||||
- If running manually: restart `pnpm run dev`
|
||||
|
||||
|
||||
## Diagnostics
|
||||
|
||||
@@ -110,8 +110,8 @@ If a merge fails badly (e.g., cannot resolve conflicts):
|
||||
# Step 4: Validation
|
||||
|
||||
After all selected skills are merged:
|
||||
- `npm run build`
|
||||
- `npm test` (do not fail the flow if tests are not configured)
|
||||
- `pnpm run build`
|
||||
- `pnpm test` (do not fail the flow if tests are not configured)
|
||||
|
||||
If build fails:
|
||||
- Show the error.
|
||||
|
||||
@@ -1,152 +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 || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
|
||||
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`.
|
||||
@@ -48,8 +48,8 @@ git remote add upstream https://github.com/qwibitai/nanoclaw.git
|
||||
```bash
|
||||
git fetch upstream skill/native-credential-proxy
|
||||
git merge upstream/skill/native-credential-proxy || {
|
||||
git checkout --theirs package-lock.json
|
||||
git add package-lock.json
|
||||
git checkout --theirs pnpm-lock.yaml
|
||||
git add pnpm-lock.yaml
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
@@ -62,7 +62,7 @@ This merges in:
|
||||
- Restored platform-aware proxy bind address detection
|
||||
- Reverted setup skill to `.env`-based credential instructions
|
||||
|
||||
If the merge reports conflicts beyond `package-lock.json`, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
If the merge reports conflicts beyond `pnpm-lock.yaml`, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Update main group CLAUDE.md
|
||||
|
||||
@@ -77,9 +77,9 @@ with:
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/credential-proxy.test.ts src/container-runner.test.ts
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm exec vitest run src/credential-proxy.test.ts src/container-runner.test.ts
|
||||
```
|
||||
|
||||
All tests must pass and build must be clean before proceeding.
|
||||
@@ -125,7 +125,7 @@ echo 'ANTHROPIC_API_KEY=<key>' >> .env
|
||||
1. Rebuild and restart:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Then restart the service:
|
||||
@@ -161,7 +161,7 @@ To revert to OneCLI gateway:
|
||||
|
||||
1. Find the merge commit: `git log --oneline --merges -5`
|
||||
2. Revert it: `git revert <merge-commit> -m 1` (undoes the skill branch merge, keeps your other changes)
|
||||
3. `npm install` (re-adds `@onecli-sh/sdk`)
|
||||
4. `npm run build`
|
||||
3. `pnpm install` (re-adds `@onecli-sh/sdk`)
|
||||
4. `pnpm run build`
|
||||
5. Follow `/setup` step 4 to configure OneCLI credentials
|
||||
6. Remove `ANTHROPIC_API_KEY` / `CLAUDE_CODE_OAUTH_TOKEN` from `.env`
|
||||
|
||||
@@ -26,7 +26,7 @@ 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
|
||||
pnpm ls playwright dotenv-cli || pnpm install playwright dotenv-cli
|
||||
```
|
||||
3. **CHROME_PATH configured** in `.env` (if Chrome is not at default location):
|
||||
```bash
|
||||
@@ -40,7 +40,7 @@ Before using this skill, ensure:
|
||||
|
||||
```bash
|
||||
# 1. Setup authentication (interactive)
|
||||
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
pnpm exec dotenv -e .env -- pnpm exec tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
# Verify: data/x-auth.json should exist after successful login
|
||||
|
||||
# 2. Rebuild container to include skill
|
||||
@@ -48,7 +48,7 @@ npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
# Verify: Output shows "COPY .claude/skills/x-integration/agent.ts"
|
||||
|
||||
# 3. Rebuild host and restart service
|
||||
npm run build
|
||||
pnpm 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)
|
||||
@@ -225,7 +225,7 @@ 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`:
|
||||
Then add COPY line after `COPY container/agent-runner/ ./` and before `RUN pnpm run build`:
|
||||
```dockerfile
|
||||
# Copy skill MCP tools
|
||||
COPY .claude/skills/x-integration/agent.ts ./src/skills/x-integration/
|
||||
@@ -247,7 +247,7 @@ 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
|
||||
pnpm exec dotenv -e .env -- pnpm exec tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
```
|
||||
|
||||
This opens Chrome for manual X login. Session saved to `data/x-browser-profile/`.
|
||||
@@ -271,7 +271,7 @@ cat data/x-auth.json # Should show {"authenticated": true, ...}
|
||||
### 4. Restart Service
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
@@ -317,26 +317,26 @@ 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
|
||||
pnpm exec dotenv -e .env -- pnpm exec 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
|
||||
echo '{"content":"Test tweet - please ignore"}' | pnpm exec dotenv -e .env -- pnpm exec 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
|
||||
echo '{"tweetUrl":"https://x.com/user/status/123"}' | pnpm exec dotenv -e .env -- pnpm exec 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
|
||||
echo '{"content":"Test"}' | pnpm exec tsx .claude/skills/x-integration/scripts/post.ts
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -344,7 +344,7 @@ echo '{"content":"Test"}' | npx tsx .claude/skills/x-integration/scripts/post.ts
|
||||
### Authentication Expired
|
||||
|
||||
```bash
|
||||
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
pnpm exec dotenv -e .env -- pnpm exec tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
@@ -22,7 +22,7 @@ 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], {
|
||||
const proc = spawn('pnpm', ['exec', 'tsx', scriptPath], {
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, NANOCLAW_ROOT: process.cwd() },
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
#!/usr/bin/env pnpm exec tsx
|
||||
/**
|
||||
* X Integration - Like Tweet
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | npx tsx like.ts
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | pnpm exec tsx like.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, navigateToTweet, runScript, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
#!/usr/bin/env pnpm exec tsx
|
||||
/**
|
||||
* X Integration - Post Tweet
|
||||
* Usage: echo '{"content":"Hello world"}' | npx tsx post.ts
|
||||
* Usage: echo '{"content":"Hello world"}' | pnpm exec tsx post.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
#!/usr/bin/env pnpm exec tsx
|
||||
/**
|
||||
* X Integration - Quote Tweet
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","comment":"My thoughts"}' | npx tsx quote.ts
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","comment":"My thoughts"}' | pnpm exec tsx quote.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, navigateToTweet, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
#!/usr/bin/env pnpm exec tsx
|
||||
/**
|
||||
* X Integration - Reply to Tweet
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","content":"Great post!"}' | npx tsx reply.ts
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","content":"Great post!"}' | pnpm exec tsx reply.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, navigateToTweet, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
#!/usr/bin/env pnpm exec tsx
|
||||
/**
|
||||
* X Integration - Retweet
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | npx tsx retweet.ts
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | pnpm exec tsx retweet.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, navigateToTweet, runScript, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
#!/usr/bin/env pnpm exec tsx
|
||||
/**
|
||||
* X Integration - Authentication Setup
|
||||
* Usage: npx tsx setup.ts
|
||||
* Usage: pnpm exec tsx setup.ts
|
||||
*
|
||||
* Interactive script - opens browser for manual login
|
||||
*/
|
||||
|
||||
6
.github/workflows/bump-version.yml
vendored
6
.github/workflows/bump-version.yml
vendored
@@ -20,10 +20,12 @@ jobs:
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Bump patch version
|
||||
run: |
|
||||
npm version patch --no-git-tag-version
|
||||
git add package.json package-lock.json
|
||||
pnpm version patch --no-git-tag-version
|
||||
git add package.json
|
||||
git diff --cached --quiet && exit 0
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -9,17 +9,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
cache: pnpm
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.12
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Install agent-runner deps (Bun)
|
||||
working-directory: container/agent-runner
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Format check
|
||||
run: npm run format:check
|
||||
run: pnpm run format:check
|
||||
|
||||
- name: Typecheck
|
||||
run: npx tsc --noEmit
|
||||
- name: Typecheck host
|
||||
run: pnpm exec tsc --noEmit
|
||||
|
||||
- name: Tests
|
||||
run: npx vitest run
|
||||
- name: Typecheck container
|
||||
run: pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit
|
||||
|
||||
- name: Host tests
|
||||
run: pnpm exec vitest run
|
||||
|
||||
- name: Container tests
|
||||
working-directory: container/agent-runner
|
||||
run: bun test
|
||||
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.npm-cache/
|
||||
# pnpm content-addressable store (created when running in sandbox mode)
|
||||
.pnpm-store/
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
@@ -9,18 +11,19 @@ store/
|
||||
data/
|
||||
logs/
|
||||
|
||||
# Groups - only track base structure and specific CLAUDE.md files
|
||||
# Groups - per-installation state, not tracked
|
||||
groups/*
|
||||
!groups/main/
|
||||
!groups/global/
|
||||
groups/main/*
|
||||
groups/global/*
|
||||
!groups/main/CLAUDE.md
|
||||
!groups/global/CLAUDE.md
|
||||
|
||||
# Composer-managed CLAUDE.md artifacts (regenerated every spawn) and
|
||||
# per-group memory (CLAUDE.local.md) must never be committed.
|
||||
**/CLAUDE.local.md
|
||||
**/.claude-shared.md
|
||||
**/.claude-fragments/
|
||||
|
||||
# Secrets
|
||||
*.keys.json
|
||||
.env
|
||||
.env*
|
||||
|
||||
# Temp files
|
||||
.tmp-*
|
||||
|
||||
@@ -1 +1 @@
|
||||
npm run format:fix
|
||||
pnpm run format:fix
|
||||
|
||||
4
.npmrc
4
.npmrc
@@ -1 +1,3 @@
|
||||
minReleaseAge=7d
|
||||
# Safety net — pnpm-workspace.yaml has the authoritative minimumReleaseAge (4320 min = 3 days)
|
||||
# This .npmrc value is a fallback if npm is ever invoked directly
|
||||
minReleaseAge=3d
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -4,6 +4,21 @@ All notable changes to NanoClaw will be documented in this file.
|
||||
|
||||
For detailed release notes, see the [full changelog on the documentation site](https://docs.nanoclaw.dev/changelog).
|
||||
|
||||
## [2.0.0] - 2026-04-22
|
||||
|
||||
Major version. NanoClaw v2 is a substantial architectural rewrite. Existing forks should run `/migrate-nanoclaw` (clean-base replay of customizations) or `/update-nanoclaw` (selective cherry-pick) before resuming work.
|
||||
|
||||
- [BREAKING] **New entity model.** Users, roles (owner/admin), messaging groups, and agent groups are now tracked as separate entities, wired via `messaging_group_agents`. Privilege is user-level instead of channel-level, so the old "main channel = admin" concept is retired. See [docs/architecture.md](docs/architecture.md) and [docs/isolation-model.md](docs/isolation-model.md).
|
||||
- [BREAKING] **Two-DB session split.** Each session now has `inbound.db` (host writes, container reads) and `outbound.db` (container writes, host reads) with exactly one writer each. Replaces the single shared session DB and eliminates cross-mount SQLite contention. See [docs/db-session.md](docs/db-session.md).
|
||||
- [BREAKING] **Install flow replaced.** `bash nanoclaw.sh` is the new default: a scripted installer that hands off to Claude Code for error recovery and guided decisions. The `/setup` Claude-guided skill still works as an alternative.
|
||||
- [BREAKING] **Channels moved to the `channels` branch.** Trunk no longer ships Discord, Slack, Telegram, WhatsApp, iMessage, Teams, Linear, GitHub, WeChat, Matrix, Google Chat, Webex, Resend, or WhatsApp Cloud. Install them per fork via `/add-<channel>` skills, which copy from the `channels` branch. `/update-nanoclaw` will re-install the channels your fork had.
|
||||
- [BREAKING] **Alternative providers moved to the `providers` branch.** OpenCode, Codex, and Ollama install via `/add-opencode`, `/add-codex`, `/add-ollama-provider`. Claude remains the default provider baked into trunk.
|
||||
- [BREAKING] **Three-level channel isolation.** Wire channels to their own agent (separate agent groups), share an agent with independent conversations (`session_mode: 'shared'`), or merge channels into one shared session (`session_mode: 'agent-shared'`). Chosen per channel via `/manage-channels`.
|
||||
- [BREAKING] **Apple Container removed from default setup.** Still available as an opt-in via `/convert-to-apple-container`.
|
||||
- **Shared-source agent-runner.** Per-group `agent-runner-src/` overlays are gone; all groups mount the same agent-runner read-only. Per-group customization flows through composed `CLAUDE.md` (shared base + per-group fragments).
|
||||
- **Agent-runner runtime moved from Node to Bun.** Container image is self-contained; no host-side impact. Host remains on Node + pnpm.
|
||||
- **OneCLI Agent Vault is the sole credential path.** Containers never receive raw API keys; credentials are injected at request time.
|
||||
|
||||
## [1.2.36] - 2026-03-26
|
||||
|
||||
- [BREAKING] Replaced pino logger with built-in logger. WhatsApp users must re-merge the WhatsApp fork to pick up the Baileys logger compatibility fix: `git fetch whatsapp main && git merge whatsapp/main`. If the `whatsapp` remote is not configured: `git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git`.
|
||||
|
||||
239
CLAUDE.md
239
CLAUDE.md
@@ -1,80 +1,247 @@
|
||||
# ⚠️ STOP — READ THIS FIRST IF YOU ARE CLAUDE ⚠️
|
||||
|
||||
**If you are reading this because you just ran `git pull`, `git merge`, `git fetch && git merge`, or any equivalent to bring in upstream changes — and you see merge conflicts or a large diff involving this file — HALT IMMEDIATELY.**
|
||||
|
||||
This is NanoClaw v2, a ground-up rewrite with breaking changes throughout. It cannot be merged into an existing v1 install. Attempting to resolve the conflicts by hand, run builds, or "fix" anything will corrupt the user's install and burn tokens for no result.
|
||||
|
||||
**Do this instead:**
|
||||
1. Abort the merge: `git merge --abort` (or `git reset --hard ORIG_HEAD` if the merge already completed).
|
||||
2. Tell the user, verbatim: *"This is the v2 rewrite — it can't be merged into your existing install. Run `bash migrate-v2.sh` to migrate instead."*
|
||||
3. Wait for the user to confirm before doing anything else. Do not attempt the migration yourself.
|
||||
|
||||
If you are a fresh install (you ran `git clone`, not `git pull`) and there are no conflicts, ignore this banner and continue below.
|
||||
|
||||
---
|
||||
|
||||
# NanoClaw
|
||||
|
||||
Personal Claude assistant. See [README.md](README.md) for philosophy and setup. See [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) for architecture decisions.
|
||||
Personal Claude assistant. See [README.md](README.md) for philosophy and setup. Architecture lives in `docs/`.
|
||||
|
||||
## Quick Context
|
||||
|
||||
Single Node.js process with skill-based channel system. Channels (WhatsApp, Telegram, Slack, Discord, Gmail) are skills that self-register at startup. Messages route to Claude Agent SDK running in containers (Linux VMs). Each group has isolated filesystem and memory.
|
||||
The host is a single Node process that orchestrates per-session agent containers. Platform messages land via channel adapters, route through an entity model (users → messaging groups → agent groups → sessions), get written into the session's inbound DB, and wake a container. The agent-runner inside the container polls the DB, calls Claude, and writes back to the outbound DB. The host polls the outbound DB and delivers through the same adapter.
|
||||
|
||||
**Everything is a message.** There is no IPC, no file watcher, no stdin piping between host and container. The two session DBs are the sole IO surface.
|
||||
|
||||
## Entity Model
|
||||
|
||||
```
|
||||
users (id "<channel>:<handle>", kind, display_name)
|
||||
user_roles (user_id, role, agent_group_id) — owner | admin (global or scoped)
|
||||
agent_group_members (user_id, agent_group_id) — unprivileged access gate
|
||||
user_dms (user_id, channel_type, messaging_group_id) — cold-DM cache
|
||||
|
||||
agent_groups (workspace, memory, CLAUDE.md, personality, container config)
|
||||
↕ many-to-many via messaging_group_agents (session_mode, trigger_rules, priority)
|
||||
messaging_groups (one chat/channel on one platform; unknown_sender_policy)
|
||||
|
||||
sessions (agent_group_id + messaging_group_id + thread_id → per-session container)
|
||||
```
|
||||
|
||||
Privilege is user-level (owner/admin), not agent-group-level. See [docs/isolation-model.md](docs/isolation-model.md) for the three isolation levels (`agent-shared`, `shared`, separate agents).
|
||||
|
||||
## Two-DB Session Split
|
||||
|
||||
Each session has **two** SQLite files under `data/v2-sessions/<session_id>/`:
|
||||
|
||||
- `inbound.db` — host writes, container reads. `messages_in`, routing, destinations, pending_questions, processing_ack.
|
||||
- `outbound.db` — container writes, host reads. `messages_out`, session_state.
|
||||
|
||||
Exactly one writer per file — no cross-mount lock contention. Heartbeat is a file touch at `/workspace/.heartbeat`, not a DB update. Host uses even `seq` numbers, container uses odd.
|
||||
|
||||
## Central DB
|
||||
|
||||
`data/v2.db` holds everything that isn't per-session: users, user_roles, agent_groups, messaging_groups, wiring, pending_approvals, user_dms, chat_sdk_* (for the Chat SDK bridge), schema_version. Migrations live at `src/db/migrations/`.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/index.ts` | Orchestrator: state, message loop, agent invocation |
|
||||
| `src/channels/registry.ts` | Channel registry (self-registration at startup) |
|
||||
| `src/ipc.ts` | IPC watcher and task processing |
|
||||
| `src/router.ts` | Message formatting and outbound routing |
|
||||
| `src/config.ts` | Trigger pattern, paths, intervals |
|
||||
| `src/container-runner.ts` | Spawns agent containers with mounts |
|
||||
| `src/task-scheduler.ts` | Runs scheduled tasks |
|
||||
| `src/db.ts` | SQLite operations |
|
||||
| `groups/{name}/CLAUDE.md` | Per-group memory (isolated) |
|
||||
| `container/skills/` | Skills loaded inside agent containers (browser, status, formatting) |
|
||||
| `src/index.ts` | Entry point: init DB, migrations, channel adapters, delivery polls, sweep, shutdown |
|
||||
| `src/router.ts` | Inbound routing: messaging group → agent group → session → `inbound.db` → wake |
|
||||
| `src/delivery.ts` | Polls `outbound.db`, delivers via adapter, handles system actions (schedule, approvals, etc.) |
|
||||
| `src/host-sweep.ts` | 60s sweep: `processing_ack` sync, stale detection, due-message wake, recurrence |
|
||||
| `src/session-manager.ts` | Resolves sessions; opens `inbound.db` / `outbound.db`; manages heartbeat path |
|
||||
| `src/container-runner.ts` | Spawns per-agent-group Docker containers with session DB + outbox mounts, OneCLI `ensureAgent` |
|
||||
| `src/container-runtime.ts` | Runtime selection (Docker vs Apple containers), orphan cleanup |
|
||||
| `src/modules/permissions/access.ts` | `canAccessAgentGroup` — owner / global admin / scoped admin / member resolution against `user_roles` + `agent_group_members` |
|
||||
| `src/modules/approvals/primitive.ts` | `pickApprover`, `pickApprovalDelivery`, `requestApproval`, approval-handler registry |
|
||||
| `src/command-gate.ts` | Router-side admin command gate — queries `user_roles` directly (no env var, no container-side check) |
|
||||
| `src/onecli-approvals.ts` | OneCLI credentialed-action approval bridge |
|
||||
| `src/user-dm.ts` | Cold-DM resolution + `user_dms` cache |
|
||||
| `src/group-init.ts` | Per-agent-group filesystem scaffold (CLAUDE.md, skills, agent-runner-src overlay) |
|
||||
| `src/db/` | DB layer — agent_groups, messaging_groups, sessions, user_roles, user_dms, pending_*, migrations |
|
||||
| `src/channels/` | Channel adapter infra (registry, Chat SDK bridge); specific channel adapters are skill-installed from the `channels` branch |
|
||||
| `src/providers/` | Host-side provider container-config (`claude` baked in; `opencode` etc. installed from the `providers` branch) |
|
||||
| `container/agent-runner/src/` | Agent-runner: poll loop, formatter, provider abstraction, MCP tools, destinations |
|
||||
| `container/skills/` | Container skills mounted into every agent session |
|
||||
| `groups/<folder>/` | Per-agent-group filesystem (CLAUDE.md, skills, per-group `agent-runner-src/` overlay) |
|
||||
| `scripts/init-first-agent.ts` | Bootstrap the first DM-wired agent (used by `/init-first-agent` skill) |
|
||||
|
||||
## Secrets / Credentials / Proxy (OneCLI)
|
||||
## Channels and Providers (skill-installed)
|
||||
|
||||
API keys, secret keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway — which handles secret injection into containers at request time, so no keys or tokens are ever passed to containers directly. Run `onecli --help`.
|
||||
Trunk does not ship any specific channel adapter or non-default agent provider. The codebase is the registry/infra; the actual adapters and providers live on long-lived sibling branches and get copied in by skills:
|
||||
|
||||
- **`channels` branch** — Discord, Slack, Telegram, WhatsApp, Teams, Linear, GitHub, iMessage, Webex, Resend, Matrix, Google Chat, WhatsApp Cloud (+ helpers, tests, channel-specific setup steps). Installed via `/add-<channel>` skills.
|
||||
- **`providers` branch** — OpenCode (and any future non-default agent providers). Installed via `/add-opencode`.
|
||||
|
||||
Each `/add-<name>` skill is idempotent: `git fetch origin <branch>` → copy module(s) into the standard paths → append a self-registration import to the relevant barrel → `pnpm install <pkg>@<pinned-version>` → build.
|
||||
|
||||
## Self-Modification
|
||||
|
||||
One tier of agent self-modification today:
|
||||
|
||||
1. **`install_packages` / `add_mcp_server`** — changes to the per-agent-group container config only (apt/npm deps, wire an existing MCP server). Single admin approval per request; on approve, the handler in `src/modules/self-mod/apply.ts` rebuilds the image when needed (`install_packages` only) and restarts the container. `container/agent-runner/src/mcp-tools/self-mod.ts`.
|
||||
|
||||
A second tier (direct source-level self-edits via a draft/activate flow) is planned but not yet implemented.
|
||||
|
||||
## Secrets / Credentials / OneCLI
|
||||
|
||||
API keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway. Secrets are injected into per-agent containers at request time — none are passed in env vars or through chat context. `src/onecli-approvals.ts`, `ensureAgent()` in `container-runner.ts`. Run `onecli --help`.
|
||||
|
||||
### Gotcha: auto-created agents start in `selective` secret mode
|
||||
|
||||
When the host first spawns a session for a new agent group, `container-runner.ts:385` calls `onecli.ensureAgent({ name, identifier })`. The OneCLI `POST /api/agents` endpoint creates the agent in **`selective`** secret mode — meaning **no secrets are assigned to it by default**, even if the secrets exist in the vault and have host patterns that would otherwise match.
|
||||
|
||||
Symptom: container starts, the proxy + CA cert are wired correctly, but the agent gets `401 Unauthorized` (or similar) from APIs whose credentials *are* in the vault. The credential just isn't in this agent's allow-list.
|
||||
|
||||
The SDK does not expose `setSecretMode` — the only fix is the CLI (or the web UI at `http://127.0.0.1:10254`).
|
||||
|
||||
```bash
|
||||
# Find the agent (identifier is the agent group id)
|
||||
onecli agents list
|
||||
|
||||
# Flip to "all" so every vault secret with a matching host pattern gets injected
|
||||
onecli agents set-secret-mode --id <agent-id> --mode all
|
||||
|
||||
# Or, stay selective and assign specific secrets
|
||||
onecli secrets list # find secret ids
|
||||
onecli agents set-secrets --id <agent-id> --secret-ids <id1>,<id2>
|
||||
|
||||
# Inspect what an agent currently has
|
||||
onecli agents secrets --id <agent-id> # secrets assigned to this agent
|
||||
onecli secrets list # all vault secrets (with host patterns)
|
||||
```
|
||||
|
||||
If you've just enabled `mode all`, no container restart is needed — the gateway looks up secrets per request, so the next API call from the running container will see the new credentials.
|
||||
|
||||
### Requiring approval for credential use
|
||||
|
||||
Approval-gating credentialed actions is a **two-sided** flow:
|
||||
|
||||
- **Server-side** (OneCLI gateway): decides *when* to hold a request and emit a pending approval. As of `onecli@1.3.0`, the CLI does **not** expose this — `rules create --action` only accepts `block` or `rate_limit`, and `secrets create` has no approval flag. Approval policies must be configured via the OneCLI web UI at `http://127.0.0.1:10254`. If/when the CLI grows an `approve` action, this section needs updating.
|
||||
- **Host-side** (nanoclaw): receives pending approvals and routes them to a human. `src/modules/approvals/onecli-approvals.ts` registers a callback via `onecli.configureManualApproval(cb)` (long-polls `GET /api/approvals/pending`). The callback uses `pickApprover` + `pickApprovalDelivery` from `src/modules/approvals/primitive.ts` to DM an approver. Approvers are resolved from the `user_roles` table — preference order: scoped admins for the agent group → global admins → owners. There is no env var like `NANOCLAW_ADMIN_USER_IDS`; roles are persisted in the central DB only.
|
||||
|
||||
If approvals are configured server-side but the host callback isn't running (or throws), every credentialed call hangs until the gateway times out. Conversely, if the gateway has no rule asking for approval, the host callback never fires regardless of how it's wired.
|
||||
|
||||
## Skills
|
||||
|
||||
Four types of skills exist in NanoClaw. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy and guidelines.
|
||||
Four types of skills. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy.
|
||||
|
||||
- **Feature skills** — merge a `skill/*` branch to add capabilities (e.g. `/add-telegram`, `/add-slack`)
|
||||
- **Utility skills** — ship code files alongside SKILL.md (e.g. `/claw`)
|
||||
- **Operational skills** — instruction-only workflows, always on `main` (e.g. `/setup`, `/debug`)
|
||||
- **Container skills** — loaded inside agent containers at runtime (`container/skills/`)
|
||||
- **Channel/provider install skills** — copy the relevant module(s) in from the `channels` or `providers` branch, wire imports, install pinned deps (e.g. `/add-discord`, `/add-slack`, `/add-whatsapp`, `/add-opencode`).
|
||||
- **Utility skills** — ship code files alongside `SKILL.md` (e.g. `/claw`).
|
||||
- **Operational skills** — instruction-only workflows (`/setup`, `/debug`, `/customize`, `/init-first-agent`, `/manage-channels`, `/init-onecli`, `/update-nanoclaw`).
|
||||
- **Container skills** — loaded inside agent containers at runtime (`container/skills/`: `welcome`, `self-customize`, `agent-browser`, `slack-formatting`).
|
||||
|
||||
| Skill | When to Use |
|
||||
|-------|-------------|
|
||||
| `/setup` | First-time installation, authentication, service configuration |
|
||||
| `/customize` | Adding channels, integrations, changing behavior |
|
||||
| `/setup` | First-time install, auth, service config |
|
||||
| `/init-first-agent` | Bootstrap the first DM-wired agent (channel pick → identity → wire → welcome DM) |
|
||||
| `/manage-channels` | Wire channels to agent groups with isolation level decisions |
|
||||
| `/customize` | Adding channels, integrations, behavior changes |
|
||||
| `/debug` | Container issues, logs, troubleshooting |
|
||||
| `/update-nanoclaw` | Bring upstream NanoClaw updates into a customized install |
|
||||
| `/init-onecli` | Install OneCLI Agent Vault and migrate `.env` credentials to it |
|
||||
| `/qodo-pr-resolver` | Fetch and fix Qodo PR review issues interactively or in batch |
|
||||
| `/get-qodo-rules` | Load org- and repo-level coding rules from Qodo before code tasks |
|
||||
| `/update-nanoclaw` | Bring upstream updates into a customized install |
|
||||
| `/init-onecli` | Install OneCLI Agent Vault and migrate `.env` credentials |
|
||||
|
||||
## Contributing
|
||||
|
||||
Before creating a PR, adding a skill, or preparing any contribution, you MUST read [CONTRIBUTING.md](CONTRIBUTING.md). It covers accepted change types, the four skill types and their guidelines, SKILL.md format rules, PR requirements, and the pre-submission checklist (searching for existing PRs/issues, testing, description format).
|
||||
Before creating a PR, adding a skill, or preparing any contribution, you MUST read [CONTRIBUTING.md](CONTRIBUTING.md). It covers accepted change types, the four skill types and their guidelines, `SKILL.md` format rules, and the pre-submission checklist.
|
||||
|
||||
## Development
|
||||
|
||||
Run commands directly—don't tell the user to run them.
|
||||
Run commands directly — don't tell the user to run them.
|
||||
|
||||
```bash
|
||||
npm run dev # Run with hot reload
|
||||
npm run build # Compile TypeScript
|
||||
./container/build.sh # Rebuild agent container
|
||||
# Host (Node + pnpm)
|
||||
pnpm run dev # Host with hot reload
|
||||
pnpm run build # Compile host TypeScript (src/)
|
||||
./container/build.sh # Rebuild agent container image (nanoclaw-agent:latest)
|
||||
pnpm test # Host tests (vitest)
|
||||
|
||||
# Agent-runner (Bun — separate package tree under container/agent-runner/)
|
||||
cd container/agent-runner && bun install # After editing agent-runner deps
|
||||
cd container/agent-runner && bun test # Container tests (bun:test)
|
||||
```
|
||||
|
||||
Container typecheck is a separate tsconfig — if you edit `container/agent-runner/src/`, run `pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit` from root (or `bun run typecheck` from `container/agent-runner/`).
|
||||
|
||||
Service management:
|
||||
```bash
|
||||
# macOS (launchd)
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # restart
|
||||
|
||||
# Linux (systemd)
|
||||
systemctl --user start nanoclaw
|
||||
systemctl --user stop nanoclaw
|
||||
systemctl --user restart nanoclaw
|
||||
systemctl --user start|stop|restart nanoclaw
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
Host logs: `logs/nanoclaw.log` (normal) and `logs/nanoclaw.error.log` (errors only — some delivery/approval failures only show up here).
|
||||
|
||||
**WhatsApp not connecting after upgrade:** WhatsApp is now a separate skill, not bundled in core. Run `/add-whatsapp` (or `npx tsx scripts/apply-skill.ts .claude/skills/add-whatsapp && npm run build`) to install it. Existing auth credentials and groups are preserved.
|
||||
## Supply Chain Security (pnpm)
|
||||
|
||||
This project uses pnpm with `minimumReleaseAge: 4320` (3 days) in `pnpm-workspace.yaml`. New package versions must exist on the npm registry for 3 days before pnpm will resolve them.
|
||||
|
||||
**Rules — do not bypass without explicit human approval:**
|
||||
- **`minimumReleaseAgeExclude`**: Never add entries without human sign-off. If a package must bypass the release age gate, the human must approve and the entry must pin the exact version being excluded (e.g. `package@1.2.3`), never a range.
|
||||
- **`onlyBuiltDependencies`**: Never add packages to this list without human approval — build scripts execute arbitrary code during install.
|
||||
- **`pnpm install --frozen-lockfile`** should be used in CI, automation, and container builds. Never run bare `pnpm install` in those contexts.
|
||||
|
||||
## Docs Index
|
||||
|
||||
| Doc | Purpose |
|
||||
|-----|---------|
|
||||
| [docs/architecture.md](docs/architecture.md) | Full architecture writeup |
|
||||
| [docs/api-details.md](docs/api-details.md) | Host API + DB schema details |
|
||||
| [docs/db.md](docs/db.md) | DB architecture overview: three-DB model, cross-mount rules, readers/writers map |
|
||||
| [docs/db-central.md](docs/db-central.md) | Central DB (`data/v2.db`) — every table + migration system |
|
||||
| [docs/db-session.md](docs/db-session.md) | Per-session `inbound.db` + `outbound.db` schemas + seq parity |
|
||||
| [docs/agent-runner-details.md](docs/agent-runner-details.md) | Agent-runner internals + MCP tool interface |
|
||||
| [docs/isolation-model.md](docs/isolation-model.md) | Three-level channel isolation model |
|
||||
| [docs/setup-wiring.md](docs/setup-wiring.md) | What's wired, what's open in the setup flow |
|
||||
| [docs/architecture-diagram.md](docs/architecture-diagram.md) | Diagram version of the architecture |
|
||||
| [docs/build-and-runtime.md](docs/build-and-runtime.md) | Runtime split (Node host + Bun container), lockfiles, image build surface, CI, key invariants |
|
||||
|
||||
## Container Build Cache
|
||||
|
||||
The container buildkit caches the build context aggressively. `--no-cache` alone does NOT invalidate COPY steps — the builder's volume retains stale files. To force a truly clean rebuild, prune the builder then re-run `./container/build.sh`.
|
||||
|
||||
## Container Runtime (Bun)
|
||||
|
||||
The agent container runs on **Bun**; the host runs on **Node** (pnpm). They communicate only via session DBs — no shared modules. Details and rationale: [docs/build-and-runtime.md](docs/build-and-runtime.md).
|
||||
|
||||
**Gotchas — trigger + action:**
|
||||
|
||||
- **Adding or bumping a runtime dep in `container/agent-runner/`** → edit `package.json`, then `cd container/agent-runner && bun install` and commit the updated `bun.lock`. Do not run `pnpm install` there — agent-runner is not a pnpm workspace.
|
||||
- **Bumping `@anthropic-ai/claude-agent-sdk`, `@modelcontextprotocol/sdk`, or any agent-runner runtime dep** → no `minimumReleaseAge` policy applies to this tree. Check the release date on npm, pin deliberately, never `bun update` blindly.
|
||||
- **Writing a new named-param SQL insert/update in the container** → use `$name` in both SQL and JS keys: `.run({ $id: msg.id })`. `bun:sqlite` does not auto-strip the prefix the way `better-sqlite3` does on the host. Positional `?` params work normally.
|
||||
- **Adding a test in `container/agent-runner/src/`** → import from `bun:test`, not `vitest`. Vitest runs on Node and can't load `bun:sqlite`. `vitest.config.ts` excludes this tree.
|
||||
- **Adding a Node CLI the agent invokes at runtime** (like `agent-browser`, `claude-code`, `vercel`) → put it in the Dockerfile's pnpm global-install block, pinned to an exact version via a new `ARG`. Don't use `bun install -g` — that bypasses the pnpm supply-chain policy.
|
||||
- **Changing the Dockerfile entrypoint or the dynamic-spawn command** (`src/container-runner.ts` line ~301) → keep `exec bun ...` so signals forward cleanly. The image has no `/app/dist`; don't reintroduce a tsc build step.
|
||||
- **Changing session-DB pragmas** (`container/agent-runner/src/db/connection.ts`) → `journal_mode=DELETE` is load-bearing for cross-mount visibility. Read the comment block at the top of the file first.
|
||||
|
||||
## CJK font support
|
||||
|
||||
Agent containers ship without CJK fonts by default (~200MB saved). If you notice signals the user works with Chinese/Japanese/Korean content — conversing in CJK, CJK timezone (e.g., `Asia/Tokyo`, `Asia/Shanghai`, `Asia/Seoul`, `Asia/Taipei`, `Asia/Hong_Kong`), system locale hint, or mentions of needing to render CJK in screenshots/PDFs/scraped pages — offer to enable it:
|
||||
|
||||
```bash
|
||||
# Ensure .env has INSTALL_CJK_FONTS=true (overwrite or append)
|
||||
grep -q '^INSTALL_CJK_FONTS=' .env && sed -i.bak 's/^INSTALL_CJK_FONTS=.*/INSTALL_CJK_FONTS=true/' .env && rm -f .env.bak || echo 'INSTALL_CJK_FONTS=true' >> .env
|
||||
|
||||
# Rebuild and restart so new sessions pick up the new image
|
||||
./container/build.sh
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# systemctl --user restart nanoclaw # Linux
|
||||
```
|
||||
|
||||
`container/build.sh` reads `INSTALL_CJK_FONTS` from `.env` and passes it through as a Docker build-arg. Without CJK fonts, Chromium-rendered screenshots and PDFs containing CJK text show tofu (empty rectangles) instead of characters.
|
||||
|
||||
126
README.md
126
README.md
@@ -12,27 +12,11 @@
|
||||
<a href="README_zh.md">中文</a> •
|
||||
<a href="README_ja.md">日本語</a> •
|
||||
<a href="https://discord.gg/VDdww8qS42"><img src="https://img.shields.io/discord/1470188214710046894?label=Discord&logo=discord&v=2" alt="Discord" valign="middle"></a> •
|
||||
<a href="repo-tokens"><img src="repo-tokens/badge.svg" alt="34.9k tokens, 17% of context window" valign="middle"></a>
|
||||
<a href="repo-tokens"><img src="repo-tokens/badge.svg" alt="repo tokens" valign="middle"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
> **🔥 New Version Preview: Chat SDK + Approval Dialogs**
|
||||
>
|
||||
> A new version of NanoClaw is available for preview, featuring Vercel Chat SDK integration (15 messaging platforms from one codebase) and one-tap approval dialogs for sensitive agent actions. [Read the announcement →](https://venturebeat.com/orchestration/should-my-enterprise-ai-agent-do-that-nanoclaw-and-vercel-launch-easier-agentic-policy-setting-and-approval-dialogs-across-15-messaging-apps)
|
||||
>
|
||||
> <details>
|
||||
> <summary>Try the preview</summary>
|
||||
>
|
||||
> ```bash
|
||||
> gh repo fork qwibitai/nanoclaw --clone && cd nanoclaw
|
||||
> git checkout v2
|
||||
> claude
|
||||
> ```
|
||||
> Then run `/setup`. Feedback welcome on [Discord](https://discord.gg/VDdww8qS42). Expect breaking changes before merge to main.
|
||||
>
|
||||
> </details>
|
||||
|
||||
## Why I Built NanoClaw
|
||||
|
||||
[OpenClaw](https://github.com/openclaw/openclaw) is an impressive project, but I wouldn't have been able to sleep if I had given complex software I didn't understand full access to my life. OpenClaw has nearly half a million lines of code, 53 config files, and 70+ dependencies. Its security is at the application level (allowlists, pairing codes) rather than true OS-level isolation. Everything runs in one Node process with shared memory.
|
||||
@@ -42,55 +26,36 @@ NanoClaw provides that same core functionality, but in a codebase small enough t
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
gh repo fork qwibitai/nanoclaw --clone
|
||||
cd nanoclaw
|
||||
claude
|
||||
git clone https://github.com/qwibitai/nanoclaw.git && cd nanoclaw && bash nanoclaw.sh
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Without GitHub CLI</summary>
|
||||
|
||||
1. Fork [qwibitai/nanoclaw](https://github.com/qwibitai/nanoclaw) on GitHub (click the Fork button)
|
||||
2. `git clone https://github.com/<your-username>/nanoclaw.git`
|
||||
3. `cd nanoclaw`
|
||||
4. `claude`
|
||||
|
||||
</details>
|
||||
|
||||
Then run `/setup`. Claude Code handles everything: dependencies, authentication, container setup and service configuration.
|
||||
|
||||
> **Note:** Commands prefixed with `/` (like `/setup`, `/add-whatsapp`) are [Claude Code skills](https://code.claude.com/docs/en/skills). Type them inside the `claude` CLI prompt, not in your regular terminal. If you don't have Claude Code installed, get it at [claude.com/product/claude-code](https://claude.com/product/claude-code).
|
||||
`nanoclaw.sh` walks you from a fresh machine to a named agent you can message. It installs Node, pnpm, and Docker if missing, registers your Anthropic credential with OneCLI, builds the agent container, and pairs your first channel (Telegram, Discord, WhatsApp, or a local CLI). If a step fails, Claude Code is invoked automatically to diagnose and resume from where it broke.
|
||||
|
||||
## Philosophy
|
||||
|
||||
**Small enough to understand.** One process, a few source files and no microservices. If you want to understand the full NanoClaw codebase, just ask Claude Code to walk you through it.
|
||||
|
||||
**Secure by isolation.** Agents run in Linux containers (Apple Container on macOS, or Docker) and they can only see what's explicitly mounted. Bash access is safe because commands run inside the container, not on your host.
|
||||
**Secure by isolation.** Agents run in Linux containers and they can only see what's explicitly mounted. Bash access is safe because commands run inside the container, not on your host.
|
||||
|
||||
**Built for the individual user.** NanoClaw isn't a monolithic framework; it's software that fits each user's exact needs. Instead of becoming bloatware, NanoClaw is designed to be bespoke. You make your own fork and have Claude Code modify it to match your needs.
|
||||
|
||||
**Customization = code changes.** No configuration sprawl. Want different behavior? Modify the code. The codebase is small enough that it's safe to make changes.
|
||||
|
||||
**AI-native.**
|
||||
- No installation wizard; Claude Code guides setup.
|
||||
- No monitoring dashboard; ask Claude what's happening.
|
||||
- No debugging tools; describe the problem and Claude fixes it.
|
||||
**AI-native, hybrid by design.** The install and onboarding flow is an optimized scripted path, fast and deterministic. When a step needs judgment, whether a failed install, a guided decision, or a customization, control hands off to Claude Code seamlessly. Beyond setup there's no monitoring dashboard or debugging UI either: describe the problem in chat and Claude Code handles it.
|
||||
|
||||
**Skills over features.** Instead of adding features (e.g. support for Telegram) to the codebase, contributors submit [claude code skills](https://code.claude.com/docs/en/skills) like `/add-telegram` that transform your fork. You end up with clean code that does exactly what you need.
|
||||
**Skills over features.** Trunk ships the registry and infrastructure, not specific channel adapters or alternative agent providers. Channels (Discord, Slack, Telegram, WhatsApp, …) live on a long-lived `channels` branch; alternative providers (OpenCode, Ollama) live on `providers`. You run `/add-telegram`, `/add-opencode`, etc. and the skill copies exactly the module(s) you need into your fork. No feature you didn't ask for.
|
||||
|
||||
**Best harness, best model.** NanoClaw runs on the Claude Agent SDK, which means you're running Claude Code directly. Claude Code is highly capable and its coding and problem-solving capabilities allow it to modify and expand NanoClaw and tailor it to each user.
|
||||
**Best harness, best model.** NanoClaw natively uses Claude Code via Anthropic's official Claude Agent SDK, so you get the latest Claude models and Claude Code's full toolset, including the ability to modify and expand your own NanoClaw fork. Other providers are drop-in options: `/add-codex` for OpenAI's Codex (ChatGPT subscription or API key), `/add-opencode` for OpenRouter, Google, DeepSeek and more via OpenCode, and `/add-ollama-provider` for local open-weight models. Provider is configurable per agent group.
|
||||
|
||||
## What It Supports
|
||||
|
||||
- **Multi-channel messaging** - Talk to your assistant from WhatsApp, Telegram, Discord, Slack, or Gmail. Add channels with skills like `/add-whatsapp` or `/add-telegram`. Run one or many at the same time.
|
||||
- **Isolated group context** - Each group has its own `CLAUDE.md` memory, isolated filesystem, and runs in its own container sandbox with only that filesystem mounted to it.
|
||||
- **Main channel** - Your private channel (self-chat) for admin control; every group is completely isolated
|
||||
- **Scheduled tasks** - Recurring jobs that run Claude and can message you back
|
||||
- **Web access** - Search and fetch content from the Web
|
||||
- **Container isolation** - Agents are sandboxed in Docker (macOS/Linux), [Docker Sandboxes](docs/docker-sandboxes.md) (micro VM isolation), or Apple Container (macOS)
|
||||
- **Credential security** - Agents never hold raw API keys. Outbound requests route through [OneCLI's Agent Vault](https://github.com/onecli/onecli), which injects credentials at request time and enforces per-agent policies and rate limits.
|
||||
- **Agent Swarms** - Spin up teams of specialized agents that collaborate on complex tasks
|
||||
- **Optional integrations** - Add Gmail (`/add-gmail`) and more via skills
|
||||
- **Multi-channel messaging** — WhatsApp, Telegram, Discord, Slack, Microsoft Teams, iMessage, Matrix, Google Chat, Webex, Linear, GitHub, WeChat, and email via Resend. Installed on demand with `/add-<channel>` skills. Run one or many at the same time.
|
||||
- **Flexible isolation** — connect each channel to its own agent for full privacy, share one agent across many channels for unified memory with separate conversations, or fold multiple channels into a single shared session so one conversation spans many surfaces. Pick per channel via `/manage-channels`. See [docs/isolation-model.md](docs/isolation-model.md).
|
||||
- **Per-agent workspace** — each agent group has its own `CLAUDE.md`, its own memory, its own container, and only the mounts you allow. Nothing crosses the boundary unless you wire it to.
|
||||
- **Scheduled tasks** — recurring jobs that run Claude and can message you back
|
||||
- **Web access** — search and fetch content from the web
|
||||
- **Container isolation** — agents are sandboxed in Docker (macOS/Linux/WSL2), with optional [Docker Sandboxes](docs/docker-sandboxes.md) micro-VM isolation or Apple Container as a macOS-native opt-in
|
||||
- **Credential security** — agents never hold raw API keys. Outbound requests route through [OneCLI's Agent Vault](https://github.com/onecli/onecli), which injects credentials at request time and enforces per-agent policies and rate limits.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -102,7 +67,7 @@ Talk to your assistant with the trigger word (default: `@Andy`):
|
||||
@Andy every Monday at 8am, compile news on AI developments from Hacker News and TechCrunch and message me a briefing
|
||||
```
|
||||
|
||||
From the main channel (your self-chat), you can manage groups and tasks:
|
||||
From a channel you own or administer, you can manage groups and tasks:
|
||||
```
|
||||
@Andy list all scheduled tasks across groups
|
||||
@Andy pause the Monday briefing task
|
||||
@@ -126,54 +91,58 @@ The codebase is small enough that Claude can safely modify it.
|
||||
|
||||
**Don't add features. Add skills.**
|
||||
|
||||
If you want to add Telegram support, don't create a PR that adds Telegram to the core codebase. Instead, fork NanoClaw, make the code changes on a branch, and open a PR. We'll create a `skill/telegram` branch from your PR that other users can merge into their fork.
|
||||
If you want to add a new channel or agent provider, don't add it to trunk. New channel adapters land on the `channels` branch; new agent providers land on `providers`. Users install them in their own fork with `/add-<name>` skills, which copy the relevant module(s) into the standard paths, wire the registration, and pin dependencies.
|
||||
|
||||
Users then run `/add-telegram` on their fork and get clean code that does exactly what they need, not a bloated system trying to support every use case.
|
||||
This keeps trunk as pure registry and infra, and every fork stays lean — users get the channels and providers they asked for and nothing else.
|
||||
|
||||
### RFS (Request for Skills)
|
||||
|
||||
Skills we'd like to see:
|
||||
|
||||
**Communication Channels**
|
||||
- `/add-signal` - Add Signal as a channel
|
||||
- `/add-signal` — Add Signal as a channel
|
||||
|
||||
## Requirements
|
||||
|
||||
- macOS, Linux, or Windows (via WSL2)
|
||||
- Node.js 20+
|
||||
- [Claude Code](https://claude.ai/download)
|
||||
- [Apple Container](https://github.com/apple/container) (macOS) or [Docker](https://docker.com/products/docker-desktop) (macOS/Linux)
|
||||
- macOS or Linux (Windows via WSL2)
|
||||
- Node.js 20+ and pnpm 10+ (the installer will install both if missing)
|
||||
- [Docker Desktop](https://docker.com/products/docker-desktop) (macOS/Windows) or Docker Engine (Linux)
|
||||
- [Claude Code](https://claude.ai/download) for `/customize`, `/debug`, error recovery during setup, and all `/add-<channel>` skills
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Channels --> SQLite --> Polling loop --> Container (Claude Agent SDK) --> Response
|
||||
messaging apps → host process (router) → inbound.db → container (Bun, Claude Agent SDK) → outbound.db → host process (delivery) → messaging apps
|
||||
```
|
||||
|
||||
Single Node.js process. Channels are added via skills and self-register at startup — the orchestrator connects whichever ones have credentials present. Agents execute in isolated Linux containers with filesystem isolation. Only mounted directories are accessible. Per-group message queue with concurrency control. IPC via filesystem.
|
||||
A single Node host orchestrates per-session agent containers. When a message arrives, the host routes it via the entity model (user → messaging group → agent group → session), writes it to the session's `inbound.db`, and wakes the container. The agent-runner inside the container polls `inbound.db`, runs Claude, and writes responses to `outbound.db`. The host polls `outbound.db` and delivers back through the channel adapter.
|
||||
|
||||
For the full architecture details, see the [documentation site](https://docs.nanoclaw.dev/concepts/architecture).
|
||||
Two SQLite files per session, each with exactly one writer — no cross-mount contention, no IPC, no stdin piping. Channels and alternative providers self-register at startup; trunk ships the registry and the Chat SDK bridge, while the adapters themselves are skill-installed per fork.
|
||||
|
||||
For the full architecture writeup see [docs/architecture.md](docs/architecture.md); for the three-level isolation model see [docs/isolation-model.md](docs/isolation-model.md).
|
||||
|
||||
Key files:
|
||||
- `src/index.ts` - Orchestrator: state, message loop, agent invocation
|
||||
- `src/channels/registry.ts` - Channel registry (self-registration at startup)
|
||||
- `src/ipc.ts` - IPC watcher and task processing
|
||||
- `src/router.ts` - Message formatting and outbound routing
|
||||
- `src/group-queue.ts` - Per-group queue with global concurrency limit
|
||||
- `src/container-runner.ts` - Spawns streaming agent containers
|
||||
- `src/task-scheduler.ts` - Runs scheduled tasks
|
||||
- `src/db.ts` - SQLite operations (messages, groups, sessions, state)
|
||||
- `groups/*/CLAUDE.md` - Per-group memory
|
||||
- `src/index.ts` — entry point: DB init, channel adapters, delivery polls, sweep
|
||||
- `src/router.ts` — inbound routing: messaging group → agent group → session → `inbound.db`
|
||||
- `src/delivery.ts` — polls `outbound.db`, delivers via adapter, handles system actions
|
||||
- `src/host-sweep.ts` — 60s sweep: stale detection, due-message wake, recurrence
|
||||
- `src/session-manager.ts` — resolves sessions, opens `inbound.db` / `outbound.db`
|
||||
- `src/container-runner.ts` — spawns per-agent-group containers, OneCLI credential injection
|
||||
- `src/db/` — central DB (users, roles, agent groups, messaging groups, wiring, migrations)
|
||||
- `src/channels/` — channel adapter infra (adapters installed via `/add-<channel>` skills)
|
||||
- `src/providers/` — host-side provider config (`claude` baked in; others via skills)
|
||||
- `container/agent-runner/` — Bun agent-runner: poll loop, MCP tools, provider abstraction
|
||||
- `groups/<folder>/` — per-agent-group filesystem (`CLAUDE.md`, skills, container config)
|
||||
|
||||
## FAQ
|
||||
|
||||
**Why Docker?**
|
||||
|
||||
Docker provides cross-platform support (macOS, Linux and even Windows via WSL2) and a mature ecosystem. On macOS, you can optionally switch to Apple Container via `/convert-to-apple-container` for a lighter-weight native runtime. For additional isolation, [Docker Sandboxes](docs/docker-sandboxes.md) run each container inside a micro VM.
|
||||
Docker provides cross-platform support (macOS, Linux and Windows via WSL2) and a mature ecosystem. On macOS, you can optionally switch to Apple Container via `/convert-to-apple-container` for a lighter-weight native runtime. For additional isolation, [Docker Sandboxes](docs/docker-sandboxes.md) run each container inside a micro VM.
|
||||
|
||||
**Can I run this on Linux or Windows?**
|
||||
|
||||
Yes. Docker is the default runtime and works on macOS, Linux, and Windows (via WSL2). Just run `/setup`.
|
||||
Yes. Docker is the default runtime and works on macOS, Linux, and Windows (via WSL2). Just run `bash nanoclaw.sh`.
|
||||
|
||||
**Is this secure?**
|
||||
|
||||
@@ -185,33 +154,28 @@ We don't want configuration sprawl. Every user should customize NanoClaw so that
|
||||
|
||||
**Can I use third-party or open-source models?**
|
||||
|
||||
Yes. NanoClaw supports any Claude API-compatible model endpoint. Set these environment variables in your `.env` file:
|
||||
Yes. The supported path is `/add-opencode` (OpenRouter, OpenAI, Google, DeepSeek, and more via OpenCode config) or `/add-ollama-provider` (local open-weight models via Ollama). Both are configurable per agent group, so different agents can run on different backends in the same install.
|
||||
|
||||
For one-off experiments, any Claude API-compatible endpoint also works via `.env`:
|
||||
|
||||
```bash
|
||||
ANTHROPIC_BASE_URL=https://your-api-endpoint.com
|
||||
ANTHROPIC_AUTH_TOKEN=your-token-here
|
||||
```
|
||||
|
||||
This allows you to use:
|
||||
- Local models via [Ollama](https://ollama.ai) with an API proxy
|
||||
- Open-source models hosted on [Together AI](https://together.ai), [Fireworks](https://fireworks.ai), etc.
|
||||
- Custom model deployments with Anthropic-compatible APIs
|
||||
|
||||
Note: The model must support the Anthropic API format for best compatibility.
|
||||
|
||||
**How do I debug issues?**
|
||||
|
||||
Ask Claude Code. "Why isn't the scheduler running?" "What's in the recent logs?" "Why did this message not get a response?" That's the AI-native approach that underlies NanoClaw.
|
||||
|
||||
**Why isn't the setup working for me?**
|
||||
|
||||
If you have issues, during setup, Claude will try to dynamically fix them. If that doesn't work, run `claude`, then run `/debug`. If Claude finds an issue that is likely affecting other users, open a PR to modify the setup SKILL.md.
|
||||
If a step fails, `nanoclaw.sh` hands off to Claude Code to diagnose and resume. If that doesn't resolve it, run `claude`, then `/debug`. If Claude identifies an issue likely to affect other users, open a PR against the relevant setup step or skill.
|
||||
|
||||
**What changes will be accepted into the codebase?**
|
||||
|
||||
Only security fixes, bug fixes, and clear improvements will be accepted to the base configuration. That's all.
|
||||
|
||||
Everything else (new capabilities, OS compatibility, hardware support, enhancements) should be contributed as skills.
|
||||
Everything else (new capabilities, OS compatibility, hardware support, enhancements) should be contributed as skills on the `channels` or `providers` branch.
|
||||
|
||||
This keeps the base system minimal and lets every user customize their installation without inheriting features they don't want.
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<a href="repo-tokens"><img src="repo-tokens/badge.svg" alt="34.9k tokens, 17% of context window" valign="middle"></a>
|
||||
</p>
|
||||
|
||||
> **注意:** この日本語訳は v1 時点のもので、最新の v2 アーキテクチャは反映されていません。最新の内容は [README.md](README.md) をご覧ください。
|
||||
|
||||
---
|
||||
|
||||
<h2 align="center">🐳 Dockerサンドボックスで動作</h2>
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<a href="https://discord.gg/VDdww8qS42"><img src="https://img.shields.io/discord/1470188214710046894?label=Discord&logo=discord&v=2" alt="Discord" valign="middle"></a> •
|
||||
<a href="repo-tokens"><img src="repo-tokens/badge.svg" alt="34.9k tokens, 17% of context window" valign="middle"></a>
|
||||
</p>
|
||||
|
||||
> **注意:** 此中文翻译对应 v1 版本,已不反映最新的 v2 架构。请参考 [README.md](README.md) 获取最新内容。
|
||||
|
||||
通过 Claude Code,NanoClaw 可以动态重写自身代码,根据您的需求定制功能。
|
||||
|
||||
**新功能:** 首个支持 [Agent Swarms(智能体集群)](https://code.claude.com/docs/en/agent-teams) 的 AI 助手。可轻松组建智能体团队,在您的聊天中高效协作。
|
||||
|
||||
2
container/.dockerignore
Normal file
2
container/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
agent-runner/node_modules
|
||||
agent-runner/dist
|
||||
21
container/CLAUDE.md
Normal file
21
container/CLAUDE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
You are a NanoClaw agent. Your name, destinations, and message-sending rules are provided in the runtime system prompt at the top of each turn.
|
||||
|
||||
## Communication
|
||||
|
||||
Be concise — every message costs the reader's attention. Prefer outcomes over play-by-play; when the work is done, the final message should be about the result, not a transcript of what you did.
|
||||
|
||||
## Workspace
|
||||
|
||||
Files you create are saved in `/workspace/agent/`. Use this for notes, research, or anything that should persist across turns in this group.
|
||||
|
||||
The file `CLAUDE.local.md` in your workspace is your per-group memory. Record things there that you'll want to remember in future sessions — user preferences, project context, recurring facts. Keep entries short and structured.
|
||||
|
||||
## Memory
|
||||
|
||||
When the user shares any substantive information with you, it must be stored somewhere you can retrieve it when relevant. If it's information that is pertinent to every single conversation turn it should be put into CLAUDE.local.md. Otherwise, create a system for storing the information depending on its type - e.g. create a file of people that the user mentions so you can keep track or a file of projects. For every file you create, add a concise reference in your CLAUDE.local.md so you'll be able to find it in future conversations.
|
||||
|
||||
A core part of your job and the main thing that defines how useful you are to the user is how well you do in creating these systems for organizing information. These are your systems that help you do your job well. Evolve them over time as needed.
|
||||
|
||||
## Conversation history
|
||||
|
||||
The `conversations/` folder in your workspace holds searchable transcripts of past sessions with this group. Use it to recall prior context when a request references something that happened before. For structured long-lived data, prefer dedicated files (`customers.md`, `preferences.md`, etc.); split any file over ~500 lines into a folder with an index.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user