mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
Compare commits
301 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 420fe5ecff | |||
| 5ed5b72f10 | |||
| 60ffe34396 | |||
| 131fc99700 | |||
| e5319b3e1e | |||
| 7169c25e70 | |||
| 3d945db6eb | |||
| 95fdec335a | |||
| 1f179e07f2 | |||
| 46b19dcf9c | |||
| c80a23e24f | |||
| 32bcc2c5ae | |||
| 7cc4ecc3be | |||
| e75af5e44d | |||
| 473f766585 | |||
| 71aab8c316 | |||
| 626c565a70 | |||
| a4573395d9 | |||
| a612c2ca24 | |||
| 4202041d0b | |||
| 1888ecc1e9 | |||
| a1b227269e | |||
| 53e8135102 | |||
| 86becf8dea | |||
| 27c52205f9 | |||
| e93292de2a | |||
| bd659fd7d6 | |||
| ac9535baa8 | |||
| bfc626be82 | |||
| c37609ffc8 | |||
| 00fb1bee4a | |||
| 4857512267 | |||
| 712720eef4 | |||
| a3376c25df | |||
| 437ba63a2f | |||
| 32a973f1cd | |||
| 03dd559786 | |||
| e0258e8c1b | |||
| 2529c3e387 | |||
| f18d0561a6 | |||
| c9dd6e050e | |||
| 22150261c5 | |||
| 7639f7b1bb | |||
| 1f3b023a5a | |||
| a38c6a962f | |||
| f04921deee | |||
| c5d0ef8b4f | |||
| 45c35a08f0 | |||
| 31063a1b4c | |||
| f2f76ab4ff | |||
| 22498ae69c | |||
| 504e5296c9 | |||
| 44e2361293 | |||
| 994b323cfa | |||
| 163f5700a5 | |||
| 211d2b5877 | |||
| 8fbf9861f1 | |||
| 2b7fef628d | |||
| 0b06343323 | |||
| 58420e16eb | |||
| d1ddfa0657 | |||
| 113caa97e4 | |||
| 3e1f226281 | |||
| 1aeb8fb4ca | |||
| cc784ff94b | |||
| e55ed0f4e8 | |||
| cdf18e608f | |||
| 8ef30ad289 | |||
| 419d04fee0 | |||
| 4c46dd3b39 | |||
| 9617087c16 | |||
| b9f95df340 | |||
| 82422d2077 | |||
| 056d308868 | |||
| 4ae9785a61 | |||
| fdece8047e | |||
| 79fd142be4 | |||
| ce80e4ec3e | |||
| a1a324097e | |||
| 39d2af9981 | |||
| 57c9bfc670 | |||
| 03684d33e2 | |||
| 81d45b5be9 | |||
| 20a24dfd13 | |||
| 75c2fde2b5 | |||
| c54c779834 | |||
| e9a427ad1a | |||
| 8ef26d323f | |||
| 77321984ba | |||
| 1eaf4f88d9 | |||
| bd6a26a002 | |||
| 6801a14736 | |||
| f60b42666f | |||
| c483860cd9 | |||
| ed5dc5ea51 | |||
| db3aa0bf1f | |||
| 4d562524cd | |||
| c60a9bef2d | |||
| 0d3326aae5 | |||
| 8430e543c1 | |||
| 63746dfeb3 | |||
| 2df81e0b32 | |||
| 42467d796d | |||
| d92d75e173 | |||
| 8d60af71d3 | |||
| 1903fab5e8 | |||
| 192a5a7569 | |||
| c36541ba6c | |||
| c02ac06258 | |||
| f304c67318 | |||
| c303b6eb14 | |||
| d16755eabc | |||
| 871bfa1809 | |||
| 9a955b9b01 | |||
| ae88d2b7c2 | |||
| 65afcdc946 | |||
| 2454444f2e | |||
| 2017589683 | |||
| af13c23a5a | |||
| 2e6dc21748 | |||
| 8676c07448 | |||
| 3db0dceb1b | |||
| d4aacfe416 | |||
| b63dd186df | |||
| e07158e194 | |||
| f0e4f07ac2 | |||
| 5a606a83d4 | |||
| 669a8444ef | |||
| 2376c88aaf | |||
| b140b3655b | |||
| 7e74bfd330 | |||
| a9f9eda9f8 | |||
| 9476a80ab0 | |||
| 53e12a627f | |||
| 7bd8c6ad41 | |||
| 4c477acca3 | |||
| 9dda75bb21 | |||
| c9fa5cdbed | |||
| 062b0cb6bf | |||
| e92b245399 | |||
| 9dc8bc5d99 | |||
| 630dd54ea9 | |||
| b59216c299 | |||
| b591d7ce96 | |||
| 09e1861a22 | |||
| 67f081671d | |||
| e83ffbc103 | |||
| 4004a6b284 | |||
| 6eb81b5737 | |||
| d8fbd3b239 | |||
| 9af9bc947a | |||
| 69939b7774 | |||
| e2dbc35a15 | |||
| 5a309a0e25 | |||
| 4a999ec973 | |||
| a2badbd525 | |||
| 9f5c37fc4c | |||
| 6941e37366 | |||
| d656b5ccc1 | |||
| 57a6491c7e | |||
| ed76d51e0b | |||
| 1dc5750ca3 | |||
| e7514edd35 | |||
| b76fd425c8 | |||
| 82cb363f84 | |||
| 320176e7e8 | |||
| 2b64fec0e6 | |||
| 9486d56b01 | |||
| 12af451069 | |||
| 8a06b01646 | |||
| c31bb02c06 | |||
| c348fabf22 | |||
| afbc20a6c4 | |||
| b36f127acc | |||
| 6f2a7314d0 | |||
| 7201fe5032 | |||
| d35386a46e | |||
| 03c4e3b672 | |||
| 8535875d0c | |||
| d7c68e04b1 | |||
| 18d0b6e53f | |||
| 5a0098edc9 | |||
| 3f0451b7b0 | |||
| 90acff28ad | |||
| e540df46e6 | |||
| a03f832dbb | |||
| 820c5067b7 | |||
| 1b652e1dc0 | |||
| b539fddbcb | |||
| 934f063aff | |||
| 32a487b96b | |||
| 751a9ed2d1 | |||
| 22d7856ce0 | |||
| ca9333d48d | |||
| 6c289c3a80 | |||
| b8cf30830b | |||
| 5702760206 | |||
| 653390d9aa | |||
| 3381509e69 | |||
| 19ce90c663 | |||
| 0918f78a0c | |||
| 4fd75860cd | |||
| 5adc9497b3 | |||
| 1d5c38d15a | |||
| 75c2e1868f | |||
| f77f9ce2c4 | |||
| 27f9f0ca32 | |||
| 0c67fbf456 | |||
| 15e356a572 | |||
| 33b5627f42 | |||
| f69979fb9e | |||
| 54bf4543f2 | |||
| 36943fbcfd | |||
| 1488c5b251 | |||
| 22ab96ccac | |||
| 391b729623 | |||
| 3703c9decb | |||
| c5cb97b761 | |||
| 761d3a1b30 | |||
| cbb4da19c7 | |||
| b752e5cd34 | |||
| a74be06956 | |||
| d4a6b4a3b5 | |||
| 67020f9fbf | |||
| 9019e4e3b8 | |||
| 8a02170b21 | |||
| db3440f662 | |||
| b2a5a58f8a | |||
| 426ae0285e | |||
| 7ef1c4f5e0 | |||
| f60bb3c3d5 | |||
| 3608f05233 | |||
| 8f28cde41d | |||
| 032ba77a7f | |||
| e9db4d461d | |||
| 584114118d | |||
| bf11109825 | |||
| 6f93b20cd1 | |||
| f23a54aea0 | |||
| 6e0653f537 | |||
| ee599b9f0c | |||
| 7b337a7a07 | |||
| 3e2895987b | |||
| 22f5d55855 | |||
| 51f50bbe85 | |||
| 4c7bc80299 | |||
| 87e89147c9 | |||
| 7b0d79a6f3 | |||
| 468c6170a0 | |||
| 4c8b9cda93 | |||
| 78bfb8df85 | |||
| a86641f69e | |||
| 59c09effcb | |||
| 001ee6ec48 | |||
| 9d97f79476 | |||
| d675859c24 | |||
| 38009be263 | |||
| 3098f28b74 | |||
| 474346e214 | |||
| 29839464bf | |||
| a3fb3beb6a | |||
| 54a8648c95 | |||
| 69348510e9 | |||
| 17a72938be | |||
| 4511644d0d | |||
| 86063e0ea0 | |||
| d1ce15a4de | |||
| 5b24dd4d2e | |||
| 0d8f7f8668 | |||
| fff32f3028 | |||
| 1bb065e655 | |||
| ea7561a978 | |||
| cfc4b6c28e | |||
| dad98b0a8f | |||
| 3e41e54e10 | |||
| 4d9f0288ee | |||
| 972edd14f6 | |||
| fd59ff0ec9 | |||
| e2e32219c9 | |||
| c601aaa947 | |||
| d43d53244f | |||
| e8326bae62 | |||
| d71ffaf7ef | |||
| 5b5ee91aa7 | |||
| 2007471f4f | |||
| 9e90c0712e | |||
| 2317302745 | |||
| b247357e0d | |||
| 4dd27adb84 | |||
| cc4f03a203 | |||
| 4bc232e513 | |||
| c9d1569702 | |||
| 5b20e2908a | |||
| 089fcea474 | |||
| bd64fd667d | |||
| f0ac7fbb6d | |||
| 207addfa19 | |||
| 4afb5bd9f1 | |||
| dfcdfcac11 | |||
| d33e514d04 | |||
| 4cb13b2b60 |
@@ -0,0 +1 @@
|
||||
{"sessionId":"bedd47ed-bfa0-41da-9a03-93d41159b4cd","pid":24606,"acquiredAt":1776194767342}
|
||||
+41
-1
@@ -1 +1,41 @@
|
||||
{}
|
||||
{
|
||||
"sandbox": {
|
||||
"enabled": false
|
||||
},
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(bash setup.sh*)",
|
||||
"Bash(git remote *)",
|
||||
"Bash(pnpm exec tsx setup/index.ts*)",
|
||||
"Bash(pnpm exec tsx scripts/init-first-agent.ts*)",
|
||||
"Bash(pnpm install @chat-adapter/*)",
|
||||
"Bash(pnpm install chat-adapter-imessage*)",
|
||||
"Bash(pnpm install @bitbasti/chat-adapter-webex*)",
|
||||
"Bash(pnpm install @resend/chat-sdk-adapter*)",
|
||||
"Bash(pnpm install @whiskeysockets/baileys*)",
|
||||
"Bash(pnpm install @beeper/chat-adapter-matrix*)",
|
||||
"Bash(pnpm install @nanoco/nanoclaw-dashboard*)",
|
||||
"Bash(pnpm install --frozen-lockfile*)",
|
||||
"Bash(pnpm 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*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ This adds:
|
||||
### Validate
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
pnpm test
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### Rebuild container
|
||||
@@ -60,7 +60,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
|
||||
### Integration Test
|
||||
|
||||
1. Start NanoClaw in dev mode: `npm run dev`
|
||||
1. Start NanoClaw in dev mode: `pnpm run dev`
|
||||
2. From the **main group** (self-chat), send exactly: `/compact`
|
||||
3. Verify:
|
||||
- The agent acknowledges compaction (e.g., "Conversation compacted.")
|
||||
@@ -104,8 +104,8 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
git clone <your-fork> /tmp/nanoclaw-test
|
||||
cd /tmp/nanoclaw-test
|
||||
claude # then run /add-compact
|
||||
npm run build
|
||||
npm test
|
||||
pnpm run build
|
||||
pnpm 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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -51,12 +51,12 @@ git fetch upstream skill/emacs
|
||||
git merge upstream/skill/emacs
|
||||
```
|
||||
|
||||
If there are merge conflicts on `package-lock.json`, resolve them by accepting the incoming
|
||||
If there are merge conflicts on `pnpm-lock.yaml`, resolve them by accepting the incoming
|
||||
version and continuing:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
@@ -73,8 +73,8 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npx vitest run src/channels/emacs.test.ts
|
||||
pnpm run build
|
||||
pnpm exec vitest run src/channels/emacs.test.ts
|
||||
```
|
||||
|
||||
Build must be clean and tests must pass before proceeding.
|
||||
@@ -158,7 +158,7 @@ If `EMACS_CHANNEL_PORT` was changed from the default, also add:
|
||||
### Restart NanoClaw
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
@@ -246,18 +246,18 @@ If NanoClaw is cloned elsewhere, update the `load`/`load-file` path in your Emac
|
||||
|
||||
## After Setup
|
||||
|
||||
If running `npm run dev` while the service is active:
|
||||
If running `pnpm run dev` while the service is active:
|
||||
|
||||
```bash
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
npm run dev
|
||||
pnpm run dev
|
||||
# When done testing:
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
|
||||
# Linux:
|
||||
# systemctl --user stop nanoclaw
|
||||
# npm run dev
|
||||
# pnpm run dev
|
||||
# systemctl --user start nanoclaw
|
||||
```
|
||||
|
||||
@@ -286,4 +286,4 @@ To remove the Emacs channel:
|
||||
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)
|
||||
6. Rebuild: `pnpm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `pnpm run build && systemctl --user restart nanoclaw` (Linux)
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
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.
|
||||
|
||||
## 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. Go to [GitHub 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
|
||||
> 4. Set up a webhook on your repo(s):
|
||||
> - Go to **Settings** > **Webhooks** > **Add webhook**
|
||||
> - Payload URL: `https://your-domain/webhook/github`
|
||||
> - Content type: `application/json`
|
||||
> - Secret: generate a random string
|
||||
> - Events: select **Issue comments**, **Pull request review comments**
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
GITHUB_TOKEN=github_pat_...
|
||||
GITHUB_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**: `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 `owner/repo` (e.g. `acme/backend`). Each PR/issue becomes its own thread automatically.
|
||||
- **supports-threads**: yes (PR and issue comment threads are native conversations)
|
||||
- **typical-use**: Webhook/notification — the agent receives PR and issue events and responds in comment threads
|
||||
- **default-isolation**: Typically shares a session with a chat channel (e.g. Slack) so the agent can summarize PRs and respond to reviews in the same context. Use a separate agent group if the repo contains sensitive code that other channels shouldn't access.
|
||||
@@ -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.
|
||||
@@ -41,8 +41,8 @@ git remote add gmail https://github.com/qwibitai/nanoclaw-gmail.git
|
||||
```bash
|
||||
git fetch gmail main
|
||||
git merge gmail/main || {
|
||||
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
|
||||
}
|
||||
```
|
||||
@@ -70,9 +70,9 @@ When you receive an email notification (messages starting with `[Email from ...`
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/channels/gmail.test.ts
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm exec vitest run src/channels/gmail.test.ts
|
||||
```
|
||||
|
||||
All tests must pass (including the new Gmail tests) and build must be clean before proceeding.
|
||||
@@ -85,11 +85,27 @@ All tests must pass (including the new Gmail tests) and build must be clean befo
|
||||
ls -la ~/.gmail-mcp/ 2>/dev/null || echo "No Gmail config found"
|
||||
```
|
||||
|
||||
If `credentials.json` already exists, skip to "Build and restart" below.
|
||||
If `credentials.json` already exists with real tokens (not `onecli-managed` values), skip to "Build and restart" below.
|
||||
|
||||
### GCP Project Setup
|
||||
|
||||
Tell the user:
|
||||
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:
|
||||
>
|
||||
@@ -120,10 +136,10 @@ Tell the user:
|
||||
Run the authorization:
|
||||
|
||||
```bash
|
||||
npx -y @gongrzhe/server-gmail-autoauth-mcp auth
|
||||
pnpm dlx @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`.
|
||||
If that fails (some versions don't have an auth subcommand), try `timeout 60 pnpm dlx @gongrzhe/server-gmail-autoauth-mcp || true`. Verify with `ls ~/.gmail-mcp/credentials.json`.
|
||||
|
||||
### Build and restart
|
||||
|
||||
@@ -142,7 +158,7 @@ cd container && ./build.sh
|
||||
Then compile and restart:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
@@ -176,7 +192,7 @@ tail -f logs/nanoclaw.log
|
||||
Test directly:
|
||||
|
||||
```bash
|
||||
npx -y @gongrzhe/server-gmail-autoauth-mcp
|
||||
pnpm dlx @gongrzhe/server-gmail-autoauth-mcp
|
||||
```
|
||||
|
||||
### OAuth token expired
|
||||
@@ -185,7 +201,7 @@ Re-authorize:
|
||||
|
||||
```bash
|
||||
rm ~/.gmail-mcp/credentials.json
|
||||
npx -y @gongrzhe/server-gmail-autoauth-mcp
|
||||
pnpm dlx @gongrzhe/server-gmail-autoauth-mcp
|
||||
```
|
||||
|
||||
### Container can't access Gmail
|
||||
@@ -206,7 +222,7 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp
|
||||
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)
|
||||
5. Rebuild: `cd container && ./build.sh && cd .. && pnpm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
|
||||
|
||||
### Channel mode
|
||||
|
||||
@@ -214,7 +230,7 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp
|
||||
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`
|
||||
5. Uninstall: `pnpm 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)
|
||||
8. Rebuild: `cd container && ./build.sh && cd .. && pnpm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
|
||||
|
||||
@@ -33,8 +33,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```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 checkout --theirs pnpm-lock.yaml
|
||||
git add pnpm-lock.yaml
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
@@ -52,9 +52,9 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/image.test.ts
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm exec vitest run src/image.test.ts
|
||||
```
|
||||
|
||||
All tests must pass and build must be clean before proceeding.
|
||||
@@ -90,5 +90,5 @@ All tests must pass and build must be clean before proceeding.
|
||||
## 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.
|
||||
- **"Image - processing failed"**: Sharp may not be installed correctly. Run `pnpm 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.
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Verify iMessage Channel
|
||||
|
||||
Send an iMessage to the account running NanoClaw. The bot should respond within a few seconds.
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: add-karpathy-llm-wiki
|
||||
description: Add a persistent wiki knowledge base to a NanoClaw group. Based on Karpathy's LLM Wiki pattern. Triggers on "add wiki", "wiki", "knowledge base", "llm wiki", "karpathy wiki".
|
||||
---
|
||||
|
||||
# Add Karpathy LLM Wiki
|
||||
|
||||
Set up a persistent wiki knowledge base on NanoClaw, based on Karpathy's LLM Wiki pattern.
|
||||
|
||||
## Step 1: Read the pattern
|
||||
|
||||
Read `${CLAUDE_SKILL_DIR}/llm-wiki.md` — this is the full LLM Wiki idea as written by Karpathy. Understand it thoroughly before proceeding. Summarize the core idea to the user briefly, then discuss what they want to build.
|
||||
|
||||
## Step 2: Choose a group
|
||||
|
||||
AskUserQuestion: "Which group should have the wiki?"
|
||||
|
||||
1. **Main group** — add to your existing main chat
|
||||
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 `pnpm exec tsx setup/index.ts --step register`.
|
||||
|
||||
## Step 3: Design collaboratively
|
||||
|
||||
Discuss with the user based on the pattern:
|
||||
- What's the wiki's domain or topic?
|
||||
- What kinds of sources will they add? (URLs, PDFs, images, voice notes, books, transcripts)
|
||||
- Do they want the full three-layer architecture or a lighter version?
|
||||
- Any specific conventions they care about? (The pattern intentionally leaves this open.)
|
||||
|
||||
Based on this discussion, create three things:
|
||||
|
||||
### 3a. Directory structure
|
||||
|
||||
Create `wiki/` and `sources/` directories in the group folder. Create initial `index.md` and `log.md` per the pattern's Indexing and Logging section. Adapt to the user's domain.
|
||||
|
||||
### 3b. Container skill
|
||||
|
||||
Create a `container/skills/wiki/SKILL.md` tailored to this user's wiki. This is the schema layer from the pattern — it tells the agent how to maintain the wiki. Base it on the pattern's Operations section (ingest, query, lint) and the conventions you agreed on with the user. Don't over-prescribe — the pattern says "your LLM figures out the rest."
|
||||
|
||||
### 3c. Group CLAUDE.md
|
||||
|
||||
Edit the group's CLAUDE.md to add a wiki section. This is critical — it's what turns the agent into a wiki maintainer. It should:
|
||||
|
||||
- Explain the wiki system concisely: what it is, the three layers (sources, wiki, schema), the three operations (ingest, query, lint)
|
||||
- Index the key files and folders (`wiki/`, `sources/`, `wiki/index.md`, `wiki/log.md`)
|
||||
- Point to the container skill for detailed workflow
|
||||
- **Ingest discipline:** Be very explicit that when the user provides multiple files or points at a folder with many files, the agent MUST process them one at a time. For each file: read it, discuss takeaways, create/update all wiki pages (summary, entities, concepts, cross-references, index, log), and completely finish with that file before moving to the next. Never batch-read all files and then process them together — this produces shallow, generic pages instead of the deep integration the pattern requires.
|
||||
|
||||
## Step 4: Source handling capabilities
|
||||
|
||||
Based on the source types the user plans to ingest (discussed in Step 3), check whether the agent can already handle those formats — some are supported natively, others need a skill (e.g. `/add-image-vision`, `/add-pdf-reader`, `/add-voice-transcription`). If a needed capability isn't installed, check if there's an available skill for it and help the user get it set up.
|
||||
|
||||
### URL handling note
|
||||
|
||||
claude has built-in `WebFetch`, but it returns a summary, not the full document. For wiki ingestion of a URL where the full text matters, the container skill and CLAUDE.md should instruct claude to use bash commands to download full files instead. For example:
|
||||
|
||||
```bash
|
||||
curl -sLo sources/filename.pdf "<url>"
|
||||
```
|
||||
|
||||
If the document is a webpage, then claude can use fetch or `agent-browser` to open the page and extract full text if available. The container skill and CLAUDE.md should note this so claude gets full content for sources rather than summaries.
|
||||
|
||||
|
||||
## Step 5: Optional lint schedule
|
||||
|
||||
AskUserQuestion: "Want periodic wiki health checks?"
|
||||
|
||||
1. **Weekly**
|
||||
2. **Monthly**
|
||||
3. **Skip** — lint manually
|
||||
|
||||
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
|
||||
pnpm exec tsx -e "
|
||||
const Database = require('better-sqlite3');
|
||||
const { CronExpressionParser } = require('cron-parser');
|
||||
const db = new Database('store/messages.db');
|
||||
const interval = CronExpressionParser.parse('<cron-expr>', { tz: process.env.TZ || 'UTC' });
|
||||
const nextRun = interval.next().toISOString();
|
||||
db.prepare('INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(
|
||||
'wiki-lint',
|
||||
'<group_folder>',
|
||||
'<chat_jid>',
|
||||
'Run a wiki lint pass per the wiki container skill. Check for contradictions, orphan pages, stale content, missing cross-references, and gaps. Report findings and offer to fix issues.',
|
||||
'cron',
|
||||
'<cron-expr>',
|
||||
'group',
|
||||
nextRun,
|
||||
'active',
|
||||
new Date().toISOString()
|
||||
);
|
||||
db.close();
|
||||
"
|
||||
```
|
||||
|
||||
Use the group's `folder` and `chat_jid` from the registered groups table. Cron expressions: `0 10 * * 0` (weekly Sunday 10am) or `0 10 1 * *` (monthly 1st at 10am).
|
||||
|
||||
## Step 6: Build and restart
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
./container/build.sh
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
Tell the user to test by sending a source to the wiki group.
|
||||
@@ -0,0 +1,75 @@
|
||||
# LLM Wiki
|
||||
|
||||
> Source: [karpathy/llm-wiki.md](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)
|
||||
|
||||
A pattern for building personal knowledge bases using LLMs.
|
||||
|
||||
This is an idea file, designed to be copied to your own LLM Agent (e.g. OpenAI Codex, Claude Code, OpenCode / Pi, etc.). Its goal is to communicate the high-level idea, with your agent building out specifics through collaboration with you.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
Most interactions with LLMs and documents follow RAG patterns: upload files, retrieve relevant chunks at query time, generate answers. The knowledge is re-derived on each question with no accumulation.
|
||||
|
||||
The concept here differs fundamentally. Rather than just retrieving from raw documents, the LLM incrementally builds and maintains a persistent wiki — a structured, interlinked markdown collection sitting between you and raw sources. When adding new material, the LLM reads it, extracts key information, and integrates it into existing wiki pages—updating entities, revising summaries, flagging contradictions, strengthening synthesis. Knowledge compiles once and stays current rather than re-deriving on every query.
|
||||
|
||||
The wiki becomes a persistent, compounding artifact. Cross-references already exist. Contradictions are flagged. Synthesis reflects everything read. The wiki enriches with every source added and question asked.
|
||||
|
||||
You source material and ask questions; the LLM maintains everything—summarizing, cross-referencing, filing, and organizing. The LLM acts as programmer; Obsidian serves as IDE; the wiki functions as codebase.
|
||||
|
||||
**Applications include:**
|
||||
- Personal: tracking goals, health, self-improvement
|
||||
- Research: deep dives over weeks/months
|
||||
- Reading: building companion wikis while progressing through books
|
||||
- Business/teams: internal wikis fed by Slack, transcripts, documents
|
||||
- Analysis: competitive research, due diligence, trip planning, hobby deep-dives
|
||||
|
||||
## Architecture
|
||||
|
||||
Three layers comprise the system:
|
||||
|
||||
**Raw sources** — immutable curated documents (articles, papers, images, data). The LLM reads but never modifies these.
|
||||
|
||||
**The wiki** — LLM-generated markdown directories containing summaries, entity pages, concept pages, comparisons, syntheses. The LLM owns this entirely, creating and updating pages while maintaining cross-references and consistency.
|
||||
|
||||
**The schema** — configuration document (e.g., CLAUDE.md) explaining wiki structure, conventions, and workflows for ingestion, querying, and maintenance. This key file transforms the LLM into disciplined wiki maintainer rather than generic chatbot.
|
||||
|
||||
## Operations
|
||||
|
||||
**Ingest:** Drop new sources into the raw collection; the LLM processes them. The agent reads sources, discusses takeaways, writes summaries, updates indexes, refreshes entity and concept pages, logs entries. Single sources might touch 10-15 wiki pages. Prefer ingesting individually while staying involved, though batch ingestion with less oversight is possible.
|
||||
|
||||
**Query:** Ask questions against the wiki. The LLM searches relevant pages, synthesizes answers with citations. Answers take various forms—markdown pages, comparison tables, slide decks, charts, canvas. Good answers can be filed back into the wiki as new pages—explorations compound in the knowledge base rather than disappearing into chat history.
|
||||
|
||||
**Lint:** Periodically health-check the wiki. Look for contradictions, stale claims superseded by newer sources, orphan pages lacking inbound links, important concepts lacking dedicated pages, missing cross-references, data gaps. The LLM suggests investigations and sources to pursue, keeping the wiki healthy as it grows.
|
||||
|
||||
## Indexing and Logging
|
||||
|
||||
Two special files help navigate the growing wiki:
|
||||
|
||||
**index.md** — content-oriented catalog of everything (each page with link, one-line summary, optional metadata like dates or source counts), organized by category. The LLM updates it on every ingest. When answering queries, read the index first to locate relevant pages before drilling deeper. This approach works surprisingly well at moderate scale (~100 sources, ~hundreds of pages) while avoiding embedding-based RAG infrastructure needs.
|
||||
|
||||
**log.md** — append-only chronological record of what happened and when (ingests, queries, lint passes). Each entry beginning with consistent prefix (e.g., `## [2026-04-02] ingest | Article Title`) becomes parseable with simple tools—`grep "^## \[" log.md | tail -5` yields last 5 entries. The log shows wiki evolution timeline and helps the LLM understand recent activity.
|
||||
|
||||
## Optional: CLI Tools
|
||||
|
||||
At scale, small tools help the LLM operate more efficiently. Search engine over wiki pages is most obvious—at small scale the index suffices, but as the wiki grows, proper search becomes necessary. qmd (https://github.com/tobi/qmd) offers local search with hybrid BM25/vector search and LLM re-ranking, entirely on-device. It includes both CLI (so LLMs can shell out) and MCP server (native tool integration). Build simpler custom search scripts as needs arise.
|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
- **Obsidian Web Clipper** converts web articles to markdown for quick source collection
|
||||
- **Download images locally:** Set attachment folder in Obsidian Settings, bind download hotkey. All images store locally; LLM views and references directly instead of relying on potentially broken URLs
|
||||
- **Obsidian's graph view** visualizes wiki connectivity—what connects to what, hub pages, orphans
|
||||
- **Marp** provides markdown-based slide deck format with Obsidian plugin integration
|
||||
- **Dataview** plugin queries page frontmatter, generating dynamic tables/lists when LLM adds YAML frontmatter
|
||||
- The wiki is simply a git-backed markdown directory—version history, branching, collaboration included
|
||||
|
||||
## Why This Works
|
||||
|
||||
Knowledge base maintenance's tedious part is bookkeeping, not reading/thinking: updating cross-references, keeping summaries current, noting data contradictions, maintaining consistency across pages. Humans abandon wikis as maintenance burden outpaces value. LLMs don't bore, don't forget updates, can touch 15 files in one pass. Wiki maintenance becomes nearly free.
|
||||
|
||||
Humans curate sources, direct analysis, ask good questions, think about meaning. LLMs handle everything else.
|
||||
|
||||
This relates in spirit to Vannevar Bush's 1945 Memex—personal curated knowledge stores with associative document trails. Bush's vision resembled this more than what the web became: private, actively curated, with connections between documents as valuable as documents themselves. Bush couldn't solve maintenance; LLMs handle that.
|
||||
|
||||
## Note
|
||||
|
||||
This document intentionally remains abstract, describing the idea rather than specific implementation. Directory structure, schema conventions, page formats, tooling—all depend on domain, preferences, and LLM choice. Everything is optional and modular. Pick what's useful; ignore what isn't. Your sources might be text-only (no image handling needed). Your wiki might stay small enough that index files suffice (no search engine required). You might want different output formats entirely. Share this with your LLM agent and work collaboratively to instantiate a version fitting your needs. This document's sole purpose is communicating the pattern; your LLM figures out the rest.
|
||||
@@ -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
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
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.
|
||||
|
||||
## Install
|
||||
|
||||
NanoClaw doesn't ship channels in trunk. This skill copies the Linear adapter in from the `channels` branch.
|
||||
|
||||
### 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
|
||||
|
||||
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. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/linear@4.26.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
> 1. Go to [Linear Settings > API Keys](https://linear.app/settings/account/security/api-keys/new)
|
||||
> 2. Create a **Personal API Key** (or use an OAuth application for team-wide access)
|
||||
> 3. Copy the API key
|
||||
> 4. Set up a webhook:
|
||||
> - Go to **Settings** > **API** > **Webhooks** > **New webhook**
|
||||
> - URL: `https://your-domain/webhook/linear`
|
||||
> - Select events: **Comment** (created, updated)
|
||||
> - Copy the signing secret
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
LINEAR_API_KEY=lin_api_...
|
||||
LINEAR_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**: `linear`
|
||||
- **terminology**: Linear has "teams" containing "issues." Each issue's comment thread is a separate conversation.
|
||||
- **how-to-find-id**: The platform ID is your team key (e.g. `ENG`). Find it in Linear under Settings > Teams. Each issue becomes its own thread automatically.
|
||||
- **supports-threads**: yes (issue comment threads are native conversations)
|
||||
- **typical-use**: Webhook/notification — the agent receives issue comment events and responds in threads
|
||||
- **default-isolation**: Typically shares a session with a chat channel (e.g. Slack) so the agent can discuss issues in the same context as team chat. Use a separate agent group if the Linear team tracks sensitive work.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Verify Linear Channel
|
||||
|
||||
@mention the bot in a Linear issue comment. The bot should respond within a few seconds.
|
||||
@@ -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
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
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. Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
1. Register a bot account on your Matrix homeserver (e.g., via Element)
|
||||
2. Get the homeserver URL (e.g., `https://matrix.org` or your self-hosted URL)
|
||||
3. Get an access token:
|
||||
- In Element: **Settings** > **Help & About** > **Access Token** (advanced)
|
||||
- Or via API: `curl -XPOST 'https://matrix.org/_matrix/client/r0/login' -d '{"type":"m.login.password","user":"botuser","password":"..."}'`
|
||||
4. Note the bot's user ID (e.g., `@botuser:matrix.org`)
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
MATRIX_BASE_URL=https://matrix.org
|
||||
MATRIX_ACCESS_TOKEN=your-access-token
|
||||
MATRIX_USER_ID=@botuser:matrix.org
|
||||
MATRIX_BOT_USERNAME=botuser
|
||||
```
|
||||
|
||||
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**: `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**: 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
|
||||
- **default-isolation**: Same agent group for rooms where you're the primary user. Separate agent group for rooms with different communities or sensitive contexts.
|
||||
@@ -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.
|
||||
@@ -87,7 +87,7 @@ done
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
./container/build.sh
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
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`.
|
||||
|
||||
```bash
|
||||
cd container/agent-runner && bun add @opencode-ai/sdk@1.4.3 && 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=latest
|
||||
```
|
||||
|
||||
**(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
|
||||
```
|
||||
|
||||
## 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` (if unset, the runner defaults to `anthropic`).
|
||||
- `OPENCODE_MODEL` — full model id, e.g. `openrouter/anthropic/claude-sonnet-4`.
|
||||
- `OPENCODE_SMALL_MODEL` — optional second model for "small" tasks.
|
||||
|
||||
Credentials: OneCLI / credential proxy patterns are unchanged. For non-`anthropic` OpenCode providers, the runner registers a placeholder API key and **`ANTHROPIC_BASE_URL`** (the credential proxy) as `baseURL` so the real key never lives in the container.
|
||||
|
||||
#### Example: OpenRouter
|
||||
|
||||
```env
|
||||
# OpenCode — host passes these into the container when agent_provider is opencode
|
||||
OPENCODE_PROVIDER=openrouter
|
||||
OPENCODE_MODEL=openrouter/anthropic/claude-sonnet-4
|
||||
OPENCODE_SMALL_MODEL=openrouter/anthropic/claude-haiku-4.5
|
||||
```
|
||||
|
||||
#### Example: Anthropic via existing proxy env
|
||||
|
||||
When `OPENCODE_PROVIDER` is `anthropic`, OpenCode uses normal Anthropic env inside the container (proxy + placeholder key pattern unchanged).
|
||||
|
||||
```env
|
||||
OPENCODE_PROVIDER=anthropic
|
||||
OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
#### Example: only a main model
|
||||
|
||||
```env
|
||||
OPENCODE_PROVIDER=openrouter
|
||||
OPENCODE_MODEL=openrouter/google/gemini-2.5-pro-preview
|
||||
```
|
||||
|
||||
#### 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
|
||||
# NanoClaw still resolves AGENT_PROVIDER from agent_groups / sessions; set agent_provider to opencode there.
|
||||
# OpenCode SDK: Zen as the upstream provider + models under opencode/…
|
||||
OPENCODE_PROVIDER=opencode
|
||||
OPENCODE_MODEL=opencode/big-pickle
|
||||
OPENCODE_SMALL_MODEL=opencode/big-pickle
|
||||
|
||||
# Point the credential proxy at Zen's Anthropic-compatible base URL (host + OneCLI must forward this host).
|
||||
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}"
|
||||
```
|
||||
|
||||
For comparison, OpenRouter uses `Authorization` + `Bearer {value}`. Zen is different by design.
|
||||
|
||||
### 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 is opaque (`ses_*` ids); stale sessions are cleared using **`isSessionInvalid`** on OpenCode-specific errors (timeouts, connection resets, not-found patterns) in addition to the poll-loop's existing recovery.
|
||||
- **`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)
|
||||
|
||||
@@ -31,8 +31,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```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 checkout --theirs pnpm-lock.yaml
|
||||
git add pnpm-lock.yaml
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
@@ -49,8 +49,8 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
|
||||
### Validate
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npx vitest run src/channels/whatsapp.test.ts
|
||||
pnpm run build
|
||||
pnpm exec vitest run src/channels/whatsapp.test.ts
|
||||
```
|
||||
|
||||
### Rebuild container
|
||||
|
||||
@@ -38,8 +38,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```bash
|
||||
git fetch whatsapp skill/reactions
|
||||
git merge whatsapp/skill/reactions || {
|
||||
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
|
||||
}
|
||||
```
|
||||
@@ -54,14 +54,14 @@ This adds:
|
||||
### Run database migration
|
||||
|
||||
```bash
|
||||
npx tsx scripts/migrate-reactions.ts
|
||||
pnpm exec tsx scripts/migrate-reactions.ts
|
||||
```
|
||||
|
||||
### 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.
|
||||
@@ -71,7 +71,7 @@ All tests must pass and build must be clean before proceeding.
|
||||
### Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Linux:
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -319,7 +319,7 @@ Also add `TELEGRAM_BOT_POOL` to the launchd plist (`~/Library/LaunchAgents/com.n
|
||||
### Step 7: Rebuild and Restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
./container/build.sh # Required — MCP tool changed
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
@@ -381,4 +381,4 @@ To remove Agent Swarm support while keeping basic Telegram:
|
||||
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)
|
||||
8. Rebuild: `pnpm run build && ./container/build.sh && launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist` (macOS) or `pnpm run build && ./container/build.sh && systemctl --user restart nanoclaw` (Linux)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -42,8 +42,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```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 checkout --theirs pnpm-lock.yaml
|
||||
git add pnpm-lock.yaml
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
@@ -60,9 +60,9 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm install --legacy-peer-deps
|
||||
npm run build
|
||||
npx vitest run src/channels/whatsapp.test.ts
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm exec vitest run src/channels/whatsapp.test.ts
|
||||
```
|
||||
|
||||
All tests must pass and build must be clean before proceeding.
|
||||
@@ -103,7 +103,7 @@ The container reads environment from `data/env/env`, not `.env` directly.
|
||||
### Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -51,12 +51,12 @@ 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
|
||||
If there are merge conflicts on `pnpm-lock.yaml`, resolve them by accepting the incoming
|
||||
version and continuing:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
@@ -74,9 +74,9 @@ This merge adds:
|
||||
### Validate
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/formatting.test.ts
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm exec vitest run src/formatting.test.ts
|
||||
```
|
||||
|
||||
All 73 tests should pass and the build should be clean before continuing.
|
||||
@@ -86,7 +86,7 @@ All 73 tests should pass and the build should be clean before continuing.
|
||||
### Rebuild and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
@@ -133,5 +133,5 @@ 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
|
||||
pnpm run build
|
||||
```
|
||||
@@ -45,7 +45,7 @@ Apple Container requires macOS. It does not work on Linux.
|
||||
grep "CONTAINER_RUNTIME_BIN" src/container-runtime.ts
|
||||
```
|
||||
|
||||
If it already shows `'container'`, the runtime is already Apple Container. Skip to Phase 3.
|
||||
If it already shows `'container'`, the runtime is already Apple Container. Skip to Phase 4.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
@@ -80,13 +80,50 @@ 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.
|
||||
|
||||
## Phase 3: Verify
|
||||
## Phase 3: Credential proxy network binding
|
||||
|
||||
Apple Container uses a bridge network (bridge100) that only exists while containers are running. The credential proxy must start before any container, so it cannot bind to the bridge IP. It must bind to `0.0.0.0`, which exposes port 3001 on all network interfaces — anyone on your local network could route API requests through the proxy using your credentials.
|
||||
|
||||
Use AskUserQuestion to ask the user:
|
||||
|
||||
**"The credential proxy needs to bind to all interfaces (0.0.0.0). Is this Mac on a trusted private network?"**
|
||||
|
||||
Options:
|
||||
1. **Yes, private/home network** — description: "No firewall rule needed."
|
||||
2. **No, shared/public network** — description: "Add a macOS firewall rule to block external access to port 3001."
|
||||
|
||||
For both options, add `CREDENTIAL_PROXY_HOST=0.0.0.0` to `.env`:
|
||||
|
||||
```bash
|
||||
grep -q 'CREDENTIAL_PROXY_HOST' .env 2>/dev/null || echo 'CREDENTIAL_PROXY_HOST=0.0.0.0' >> .env
|
||||
```
|
||||
|
||||
If they chose the public network option, set up and persist the firewall rule:
|
||||
|
||||
```bash
|
||||
echo "block in on en0 proto tcp to any port 3001" | sudo pfctl -ef -
|
||||
```
|
||||
|
||||
```bash
|
||||
grep -q 'nanoclaw proxy' /etc/pf.conf 2>/dev/null || echo '# nanoclaw proxy — block LAN access to credential proxy
|
||||
block in on en0 proto tcp to any port 3001' | sudo tee -a /etc/pf.conf > /dev/null
|
||||
```
|
||||
|
||||
Verify the rule is working:
|
||||
|
||||
```bash
|
||||
curl -sf http://$(ipconfig getifaddr en0):3001 && echo "EXPOSED — rule not working" || echo "BLOCKED — rule active"
|
||||
```
|
||||
|
||||
If the verification shows "EXPOSED", warn the user and retry. If "BLOCKED", confirm success and continue.
|
||||
|
||||
## Phase 4: Verify
|
||||
|
||||
### Ensure Apple Container runtime is running
|
||||
|
||||
@@ -135,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
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
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. Resolves the session (creates `inbound.db` / `outbound.db`).
|
||||
6. Writes a `kind: 'chat'`, `sender: 'system'` welcome message into `inbound.db`.
|
||||
|
||||
Show the script's output to the user.
|
||||
|
||||
## 5. Verify
|
||||
|
||||
Host sweep runs every ~60s. Within one sweep window the container wakes, the agent processes the system 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 within ~60 seconds. 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.
|
||||
@@ -17,13 +17,7 @@ This skill installs OneCLI, configures the Agent Vault gateway, and migrates any
|
||||
onecli version 2>/dev/null
|
||||
```
|
||||
|
||||
If the command succeeds, OneCLI is installed. Check if the gateway is reachable:
|
||||
|
||||
```bash
|
||||
curl -sf http://127.0.0.1:10254/health
|
||||
```
|
||||
|
||||
If both succeed, check for an Anthropic secret:
|
||||
If the command succeeds, OneCLI is installed, check for an Anthropic secret:
|
||||
|
||||
```bash
|
||||
onecli secrets list
|
||||
@@ -81,16 +75,16 @@ Re-verify with `onecli version`.
|
||||
|
||||
### Configure the CLI
|
||||
|
||||
Point the CLI at the local OneCLI instance:
|
||||
Point the CLI at the local OneCLI instance, the ONECLI_URL was output from the install script above:
|
||||
|
||||
```bash
|
||||
onecli config set api-host http://127.0.0.1:10254
|
||||
onecli config set api-host ${ONECLI_URL}
|
||||
```
|
||||
|
||||
### Set ONECLI_URL in .env
|
||||
|
||||
```bash
|
||||
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=http://127.0.0.1:10254' >> .env
|
||||
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=${ONECLI_URL}' >> .env
|
||||
```
|
||||
|
||||
### Wait for gateway readiness
|
||||
@@ -99,7 +93,7 @@ The gateway may take a moment to start after installation. Poll for up to 15 sec
|
||||
|
||||
```bash
|
||||
for i in $(seq 1 15); do
|
||||
curl -sf http://127.0.0.1:10254/health && break
|
||||
curl -sf ${ONECLI_URL}/health && break
|
||||
sleep 1
|
||||
done
|
||||
```
|
||||
@@ -214,7 +208,7 @@ Tell the user to run `claude setup-token` in another terminal and copy the token
|
||||
|
||||
Once they have the token, AskUserQuestion with two options:
|
||||
|
||||
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
|
||||
1. **Dashboard** — description: "Best if you have a browser on this machine. Open ${ONECLI_URL} and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
|
||||
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_TOKEN --host-pattern api.anthropic.com`"
|
||||
|
||||
#### API key path
|
||||
@@ -223,7 +217,7 @@ Tell the user to get an API key from https://console.anthropic.com/settings/keys
|
||||
|
||||
AskUserQuestion with two options:
|
||||
|
||||
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI."
|
||||
1. **Dashboard** — description: "Best if you have a browser on this machine. Open ${ONECLI_URL} and add the secret in the UI."
|
||||
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_KEY --host-pattern api.anthropic.com`"
|
||||
|
||||
#### After either path
|
||||
@@ -237,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`
|
||||
@@ -262,12 +256,12 @@ If the service is running and a channel is configured, tell the user to send a t
|
||||
Tell the user:
|
||||
- OneCLI Agent Vault is now managing credentials
|
||||
- Agents never see raw API keys — credentials are injected at the gateway level
|
||||
- To manage secrets: `onecli secrets list`, or open http://127.0.0.1:10254
|
||||
- To manage secrets: `onecli secrets list`, or open ${ONECLI_URL}
|
||||
- To add rate limits or policies: `onecli rules create --help`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"OneCLI gateway not reachable" in logs:** The gateway isn't running. Check with `curl -sf http://127.0.0.1:10254/health`. Start it with `onecli start` if needed.
|
||||
**"OneCLI gateway not reachable" in logs:** The gateway isn't running. Check with `curl -sf ${ONECLI_URL}/health`. Start it with `onecli start` if needed.
|
||||
|
||||
**Container gets no credentials:** Verify `ONECLI_URL` is set in `.env` and the gateway has an Anthropic secret (`onecli secrets list`).
|
||||
|
||||
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -0,0 +1,100 @@
|
||||
# Migrating OpenClaw Cron Jobs to NanoClaw Scheduled Tasks
|
||||
|
||||
This file is referenced by SKILL.md Phase 5 when cron jobs are detected.
|
||||
|
||||
**Before inserting tasks:** Read `src/db.ts` and search for `scheduled_tasks` to verify the current table schema. The schema below is a reference — if columns have been added, removed, or renamed, use the current schema from the source code.
|
||||
|
||||
Also verify the `createTask` function signature in `src/db.ts` — it may be simpler to call it via a script than raw SQL.
|
||||
|
||||
## OpenClaw Cron Job Format
|
||||
|
||||
Source: `<STATE_DIR>/cron/jobs.json` (from `src/cron/types.ts`). If the file format doesn't match what's described below, read the actual file and adapt — OpenClaw may have changed the schema.
|
||||
|
||||
The jobs file is `{ version: 1, jobs: CronJob[] }`. Each job has:
|
||||
- `id`, `name`, `description`, `enabled`, `deleteAfterRun`
|
||||
- `schedule`: `{ kind: "cron", expr: string, tz?: string }` | `{ kind: "every", everyMs: number }` | `{ kind: "at", at: string }`
|
||||
- `payload`: `{ kind: "agentTurn", message: string, model?, thinking?, timeoutSeconds? }` | `{ kind: "systemEvent", text: string }`
|
||||
- `sessionTarget`: `"main"` | `"isolated"` | `"current"` | `"session:<id>"`
|
||||
- `wakeMode`: `"next-heartbeat"` | `"now"`
|
||||
- `delivery`: `{ mode: "none" | "announce" | "webhook", channel?, to?, threadId?, bestEffort? }`
|
||||
- `failureAlert`: `{ after?: number, channel?, to?, cooldownMs? }` | `false`
|
||||
- `state`: runtime state (nextRunAtMs, lastRunStatus, consecutiveErrors, etc.)
|
||||
|
||||
## NanoClaw `scheduled_tasks` Table
|
||||
|
||||
Source: `src/db.ts`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | TEXT PK | Unique task ID |
|
||||
| `group_folder` | TEXT | Target group directory (e.g. `"main"`) |
|
||||
| `chat_jid` | TEXT | Target chat JID |
|
||||
| `prompt` | TEXT | Task instructions |
|
||||
| `script` | TEXT | Optional bash pre-check script |
|
||||
| `schedule_type` | TEXT | `"cron"`, `"interval"`, or `"once"` |
|
||||
| `schedule_value` | TEXT | Cron expr, ms interval, or ISO timestamp |
|
||||
| `context_mode` | TEXT | `"group"` or `"isolated"` (default) |
|
||||
| `next_run` | TEXT | ISO timestamp — must be computed at insert time |
|
||||
| `last_run` | TEXT | null initially |
|
||||
| `last_result` | TEXT | null initially |
|
||||
| `status` | TEXT | `"active"`, `"paused"`, or `"completed"` |
|
||||
| `created_at` | TEXT | ISO timestamp |
|
||||
|
||||
## Field Mapping
|
||||
|
||||
- `schedule.kind:"cron"` + `schedule.expr` → `schedule_type:"cron"`, `schedule_value:<expr>`
|
||||
- `schedule.kind:"every"` + `schedule.everyMs` → `schedule_type:"interval"`, `schedule_value:<ms as string>`
|
||||
- `schedule.kind:"at"` + `schedule.at` → `schedule_type:"once"`, `schedule_value:<ISO timestamp>`
|
||||
- `payload.message` or `payload.text` → `prompt`
|
||||
- `sessionTarget:"isolated"` → `context_mode:"isolated"`, `sessionTarget:"main"` or `"current"` → `context_mode:"group"`
|
||||
|
||||
## What Doesn't Map
|
||||
|
||||
- `delivery.mode:"webhook"` — NanoClaw has no webhook delivery. Discuss with the user: this could be implemented as a task `script` that runs `curl` to hit the webhook endpoint.
|
||||
- `failureAlert` — NanoClaw has no failure alert system. Note this to the user.
|
||||
- `wakeMode` — NanoClaw tasks always wake the agent immediately.
|
||||
- `payload.model`, `payload.thinking`, `payload.timeoutSeconds` — NanoClaw doesn't support per-task model/thinking config. These are handled by the SDK.
|
||||
- `deleteAfterRun` — NanoClaw `"once"` tasks are marked `"completed"` after running, not deleted.
|
||||
|
||||
## For Each Enabled Job
|
||||
|
||||
1. Show what it does: name, schedule, prompt, delivery mode
|
||||
2. Explain any differences (no retry config, no webhook delivery, no failure alerts)
|
||||
3. If `delivery.mode:"webhook"`: discuss with the user — a task `script` with `curl` often suffices
|
||||
4. Ask if they want to keep this task
|
||||
|
||||
## Inserting Tasks
|
||||
|
||||
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
|
||||
pnpm exec tsx -e "
|
||||
const Database = require('better-sqlite3');
|
||||
const { CronExpressionParser } = require('cron-parser');
|
||||
const db = new Database('store/messages.db');
|
||||
// Compute next_run for cron tasks:
|
||||
// const interval = CronExpressionParser.parse('<expr>', { tz: process.env.TZ || 'UTC' });
|
||||
// const nextRun = interval.next().toISOString();
|
||||
db.prepare(\`INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, script, schedule_type, schedule_value, context_mode, next_run, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\`).run(
|
||||
'migrated-<original-id>',
|
||||
'<group_folder>',
|
||||
'<chat_jid>',
|
||||
'<mapped prompt>',
|
||||
null,
|
||||
'<mapped schedule_type>',
|
||||
'<mapped schedule_value>',
|
||||
'<mapped context_mode>',
|
||||
'<computed next_run ISO>',
|
||||
'active',
|
||||
new Date().toISOString()
|
||||
);
|
||||
db.close();
|
||||
"
|
||||
```
|
||||
|
||||
**Computing `next_run`:**
|
||||
- `cron` tasks: use `CronExpressionParser.parse(expr, { tz }).next().toISOString()`
|
||||
- `interval` tasks: `new Date(Date.now() + ms).toISOString()`
|
||||
- `once` tasks: `next_run` equals `schedule_value`
|
||||
|
||||
If groups haven't been registered yet (database doesn't exist), save the task details to `groups/main/openclaw-migration-tasks.md` with the exact SQL payloads, and tell the user: "These tasks will be created after `/setup` registers your groups."
|
||||
@@ -0,0 +1,447 @@
|
||||
---
|
||||
name: migrate-from-openclaw
|
||||
description: Migrate from OpenClaw to NanoClaw. Detects existing OpenClaw installation, extracts identity, channel credentials, scheduled tasks, and other config, then guides interactive migration. Triggers on "migrate from openclaw", "openclaw migration", "import from openclaw".
|
||||
---
|
||||
|
||||
# Migrate from OpenClaw
|
||||
|
||||
Guide the user through migrating their OpenClaw installation to NanoClaw. This is a conversation, not a batch job. Read OpenClaw state, discuss it with the user, make judgment calls together about what to bring over and how.
|
||||
|
||||
**Principle:** Never silently copy data. Read it, explain it, discuss where it belongs in NanoClaw's architecture, show proposed changes before applying. Credentials must be masked when displayed (first 4 + `...` + last 4 characters). Make judgment calls about what's core vs. reference material.
|
||||
|
||||
**UX:** Use `AskUserQuestion` for multiple-choice only. Use plain text for free-form input. Don't dump raw data — summarize and explain conversationally.
|
||||
|
||||
## Migration State File
|
||||
|
||||
Create `migration-state.md` in the project root at the start of Phase 0. Update it after each phase completes. This file is the single source of truth for the migration — if context is compacted or lost, re-read it to recover all decisions and progress.
|
||||
|
||||
Before starting any phase, re-read `migration-state.md` to ensure you have current state.
|
||||
|
||||
Sections to maintain (add data as each phase completes):
|
||||
|
||||
- **Progress** — checkbox list of phases (Phase 0–7)
|
||||
- **Discovery** — STATE_DIR, IDENTITY_NAME, channels, groups (with JID mappings), workspace files, cron job count, MCP servers
|
||||
- **Decisions** — assistant_name, group_model (shared/separate/main-only), main_group (folder + jid)
|
||||
- **Registered Groups** — table: folder, jid, channel, is_main
|
||||
- **Settings Migrated** — timezone, anthropic_credential (masked), sender_allowlist (created/skipped)
|
||||
- **Identity & Memory** — paths of files created, which CLAUDE.md was edited
|
||||
- **Channel Credentials** — table: channel, status, env_var
|
||||
- **Scheduled Tasks** — table: original_id, name, migrated/deferred
|
||||
- **Deferred / Not Applicable** — unsupported channels, discussed customizations, OpenClaw-only features
|
||||
|
||||
Keep it factual and terse — this is for machine recovery after compaction, not human reading. Delete the file at the end of Phase 7 (or offer to keep it as a record).
|
||||
|
||||
## Phase 0: Discovery
|
||||
|
||||
Run the discovery script to find and summarize the OpenClaw installation:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/discover-openclaw.ts
|
||||
```
|
||||
|
||||
If the user specifies a custom path, pass it: `--state-dir <path>`
|
||||
|
||||
Parse the status block. Key fields: STATUS, STATE_DIR, CHANNELS, WORKSPACE_FILES, DAILY_MEMORY_FILES, SKILL_COUNT, SKILLS, CRON_JOBS, MCP_SERVERS, IDENTITY_NAME, AGENT_COUNT, AGENT_IDS.
|
||||
|
||||
**Sanity-check the output:** The discovery script detects known structures but can silently miss data if OpenClaw's format has changed. Check `CONFIG_TOP_KEYS` and `CONFIG_CHANNEL_KEYS` — if you see keys the script didn't report on (e.g. a channel name not in CHANNELS, or a top-level section like `integrations` or `plugins`), read that section of the config directly with the Read tool. Also check `STATE_DIR_CONTENTS` for directories the script doesn't scan (e.g. unexpected folders alongside `workspace/`, `agents/`, `cron/`).
|
||||
|
||||
**If STATUS=not_found:** Tell the user no OpenClaw installation was detected at the standard locations (`~/.openclaw`, `~/.clawdbot`). Ask if they have a custom path. If not, exit.
|
||||
|
||||
**If STATUS=found:** Present a human-readable summary:
|
||||
|
||||
- "I found your OpenClaw installation at `<STATE_DIR>`."
|
||||
- Identity: name from IDENTITY.md (if found)
|
||||
- Workspace files: which of SOUL.md, USER.md, MEMORY.md, IDENTITY.md exist
|
||||
- Channels: list each, note which NanoClaw supports (whatsapp, telegram, slack, discord) and which it doesn't
|
||||
- Daily memory files: count (if any)
|
||||
- Skills: count and names (from workspace, shared, personal, project locations)
|
||||
- Cron jobs: count and names
|
||||
- MCP servers: count and names
|
||||
- Agents: count (relevant for Phase 1 groups discussion)
|
||||
|
||||
Then explain the key architectural differences. Don't dump a table — paraphrase conversationally:
|
||||
|
||||
- **Container isolation:** NanoClaw runs each agent in an isolated Linux container (Docker or Apple Container). OpenClaw runs everything in one process. This means stronger isolation but also means each group is its own sandbox.
|
||||
- **Group-based memory:** In OpenClaw, all groups under one agent share the same SOUL.md, MEMORY.md, and IDENTITY.md. In NanoClaw, each group has its own filesystem and CLAUDE.md. Shared state goes in `groups/global/CLAUDE.md` (mounted read-only into all non-main containers).
|
||||
- **Channel skills:** In OpenClaw, channels are configured in `openclaw.json`. In NanoClaw, channels are installed as code via skills (`/add-telegram`, `/add-whatsapp`, etc.) and configured through `.env` variables.
|
||||
- **Simpler config:** NanoClaw has no config file — behavior is in the code and `CLAUDE.md` files. Credentials live in `.env` or the OneCLI vault.
|
||||
|
||||
AskUserQuestion: "Ready to start migrating? I'll go through each area one at a time."
|
||||
1. **Yes, let's go** — proceed to Phase 1
|
||||
2. **Tell me more** — explain more about any area they ask about
|
||||
3. **Skip migration** — exit
|
||||
|
||||
## Phase 1: Groups and Architecture
|
||||
|
||||
**This discussion must happen before identity/memory, because the shared-vs-isolated decision determines where files go.**
|
||||
|
||||
If GROUP_COUNT > 0 or AGENT_COUNT > 1, this is a critical conversation. Even with just one group, explain the model difference so the user understands what they're getting into.
|
||||
|
||||
**OpenClaw model:** All groups routed to the same agent share one workspace — the same SOUL.md, MEMORY.md, IDENTITY.md, and tools. When you talk to the bot in your family chat or your work chat, it's the same agent with the same personality and memory. Only the session (conversation history) is separate per group.
|
||||
|
||||
**NanoClaw model:** Each group is a completely separate agent running in its own Linux container. Separate filesystem, separate memory, separate CLAUDE.md. The bot in your family chat and your work chat are different agents that don't know about each other — unless you explicitly share state via `groups/global/CLAUDE.md`, which is mounted read-only into all non-main containers.
|
||||
|
||||
Explain this conversationally. If the user only has one group, it's simple — just note the difference and move on. If they have multiple groups, discuss:
|
||||
|
||||
AskUserQuestion: "In OpenClaw, your groups shared the same personality and memory. In NanoClaw, each group is a fully separate agent. How would you like to handle this?"
|
||||
|
||||
1. **Shared personality (recommended if your groups had the same bot)** — "I'll put the shared personality, identity, and user context in `groups/global/CLAUDE.md`. Every group sees it. Each group can add its own customizations on top."
|
||||
2. **Fully separate** — "Each group gets its own independent personality and memory. Complete isolation between groups."
|
||||
3. **Just main group for now** — "Set up one group now. We can add others later."
|
||||
|
||||
Remember this choice — it determines where identity and memory files go in the next phase.
|
||||
|
||||
### Confirm assistant name
|
||||
|
||||
Before registering groups, confirm the assistant name — it's used for trigger patterns and CLAUDE.md templates.
|
||||
|
||||
IDENTITY_NAME from discovery gives the OpenClaw name. Ask the user: "Your OpenClaw assistant was named `<IDENTITY_NAME>`. Want to keep this name in NanoClaw?" If they want a different name, ask what it should be. If IDENTITY_NAME was empty, ask them to choose a name (default: "Andy").
|
||||
|
||||
The register step's `--assistant-name` flag writes `ASSISTANT_NAME` to `.env` and updates CLAUDE.md templates automatically — no manual `.env` write needed.
|
||||
|
||||
### Registering groups
|
||||
|
||||
The discovery script provides detected groups in the GROUPS field (format: `channel:id(name)=>nanoclaw_jid`). These are extracted from OpenClaw's session store and channel config.
|
||||
|
||||
For each group the user wants to bring over, pre-register it:
|
||||
|
||||
```bash
|
||||
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).
|
||||
|
||||
Folder naming: `<channel>_<name-slug>` (e.g. `whatsapp_family-chat`, `telegram_dev-team`). Ask the user to confirm each group's name and folder.
|
||||
|
||||
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: `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.
|
||||
|
||||
## Phase 2: Settings from Config
|
||||
|
||||
Before identity/memory, extract settings from `openclaw.json` that map directly to NanoClaw setup. Read the config file with the Read tool (`<STATE_DIR>/openclaw.json` or `clawdbot.json`).
|
||||
|
||||
### Timezone
|
||||
|
||||
Check `agents.defaults.userTimezone` in the config. If present and it's a valid IANA timezone (e.g. `America/New_York`, `Asia/Jerusalem`), write it to `.env` as `TZ=<timezone>`. NanoClaw's setup step 2a reads `TZ` from `.env` (`src/config.ts:84-97`) and will skip the autodetection prompt.
|
||||
|
||||
### Anthropic Credentials
|
||||
|
||||
Check for Anthropic API keys or tokens in OpenClaw's auth system. OpenClaw stores credentials in `<STATE_DIR>/auth-profiles.json` or `<STATE_DIR>/agents/main/agent/auth-profiles.json` with this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"profiles": {
|
||||
"anthropic:default": {
|
||||
"type": "api_key", // or "token" or "oauth"
|
||||
"provider": "anthropic",
|
||||
"key": "sk-ant-..." // for api_key type
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Profile IDs follow `provider:identifier` format. Look for any profile where `provider` is `"anthropic"`. The credential field depends on the `type`:
|
||||
- `type: "api_key"` → `key` field (or `keyRef` for SecretRef)
|
||||
- `type: "token"` → `token` field (or `tokenRef` for SecretRef)
|
||||
- `type: "oauth"` → `access` field (OAuth access token, may need refresh)
|
||||
|
||||
Also check:
|
||||
1. `<STATE_DIR>/.env` — for `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN`
|
||||
2. Config `models.providers` — for Anthropic provider entries with `apiKey`
|
||||
|
||||
If found, offer to save to `.env`. This pre-fills the NanoClaw setup credential step (step 4) so the user doesn't need to re-enter it. Use the same masking approach — show first 4 + last 4 characters, write the full value directly.
|
||||
|
||||
**Important:** If the credential uses `keyRef`/`tokenRef` with `source:"exec"` or `source:"file"`, explain that it can't be auto-extracted and the user will need to enter it during setup. For `type: "oauth"` credentials with an expiry in the past, warn the user the token may need to be refreshed during setup.
|
||||
|
||||
### Sender Allowlists
|
||||
|
||||
Read the channel configs for access control settings. OpenClaw stores these per-channel:
|
||||
- `channels.<channel>.allowFrom` — array of allowed sender IDs (E.164 for WhatsApp, numeric IDs for Telegram)
|
||||
- `channels.<channel>.dmPolicy` — `"open"`, `"allowlist"`, `"disabled"`
|
||||
- `channels.<channel>.groupPolicy` — `"open"`, `"allowlist"`, `"disabled"`
|
||||
- `channels.<channel>.groupAllowFrom` — array of allowed group member IDs
|
||||
|
||||
NanoClaw uses `~/.config/nanoclaw/sender-allowlist.json` with this format:
|
||||
```json
|
||||
{
|
||||
"default": { "allow": "*", "mode": "trigger" },
|
||||
"chats": {
|
||||
"<chat-jid>": {
|
||||
"allow": ["sender-id-1", "sender-id-2"],
|
||||
"mode": "trigger"
|
||||
}
|
||||
},
|
||||
"logDenied": true
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
- `allow`: `"*"` (all senders) or `string[]` (specific sender IDs)
|
||||
- `mode`: `"trigger"` (messages stored but trigger blocked for non-allowed senders) or `"drop"` (messages silently discarded before storage)
|
||||
- `logDenied`: optional boolean (default `true`), logs denied messages
|
||||
|
||||
If OpenClaw had allowlists configured, show the user what was set and offer to create the NanoClaw equivalent. Map:
|
||||
- `dmPolicy:"allowlist"` + `allowFrom` → per-chat entry with `"allow"` array, `"mode": "trigger"`
|
||||
- `groupPolicy:"allowlist"` + `groupAllowFrom` → per-group entry with `"allow"` array, `"mode": "trigger"`
|
||||
- `dmPolicy:"open"` → `"allow": "*"`
|
||||
- `dmPolicy:"disabled"` → per-chat entry with `"allow": []`, `"mode": "drop"` (or don't register that chat)
|
||||
|
||||
Create the directory and file:
|
||||
```bash
|
||||
mkdir -p ~/.config/nanoclaw
|
||||
```
|
||||
|
||||
Then write the JSON file. If no allowlists were configured, skip this.
|
||||
|
||||
### Container Timeout
|
||||
|
||||
Check `agents.defaults.timeoutSeconds` in the config. This is maximum total agent runtime (wall-clock). NanoClaw's equivalent is `CONTAINER_TIMEOUT` (env var, default 30 min), also configurable per-group via `containerConfig.timeout`. Note: NanoClaw also has a separate `IDLE_TIMEOUT` (max time without output) which resets on activity — OpenClaw has no equivalent.
|
||||
|
||||
If the OpenClaw value differs significantly from 30 minutes, note it for the user. They can set `CONTAINER_TIMEOUT=<ms>` in `.env` after setup.
|
||||
|
||||
## Phase 3: Identity and Memory
|
||||
|
||||
This phase is fully conversational — read files directly and discuss with the user. No script needed.
|
||||
|
||||
**Where files go depends on the Phase 1 (groups) decision:**
|
||||
- **Shared personality:** Core identity goes in `groups/global/CLAUDE.md` (seen by all groups). Group-specific customizations go in each group's own CLAUDE.md.
|
||||
- **Fully separate:** Everything goes in `groups/main/` (or each group's own folder).
|
||||
- **Just main group:** Everything goes in `groups/main/`.
|
||||
|
||||
### Find workspace files
|
||||
|
||||
The STATE_DIR from discovery tells you where OpenClaw lives. Look for workspace files at `<STATE_DIR>/workspace/`. If AGENT_COUNT > 1, also check `<STATE_DIR>/agents/*/workspace/` and ask which agent to migrate.
|
||||
|
||||
Use the Read tool to look at each file found.
|
||||
|
||||
### IDENTITY.md
|
||||
|
||||
Read `<STATE_DIR>/workspace/IDENTITY.md` if it exists. It uses a key:value format (name, emoji, creature, vibe, etc.).
|
||||
|
||||
The assistant name was already confirmed and written to `.env` in Phase 1. Here, focus on the rest of the identity — create an `identity.md` file with the full identity details (emoji, creature, vibe, personality traits, etc.). If shared personality was chosen in Phase 1, put it alongside `groups/global/CLAUDE.md`. Otherwise, put it in `groups/main/`.
|
||||
|
||||
### SOUL.md
|
||||
|
||||
Read `<STATE_DIR>/workspace/SOUL.md` if it exists. Then read `groups/main/CLAUDE.md`.
|
||||
|
||||
CLAUDE.md is always loaded into the agent's context — it's the agent's continuous instructions. Not everything from SOUL.md needs to be there. Discuss with the user what belongs where:
|
||||
|
||||
- **In CLAUDE.md (always loaded):** Core personality traits, communication style, key behavioral rules. Weave these into the existing CLAUDE.md structure — adjust the opening description under the `# <Name>` heading, modify the tone in the Communication section.
|
||||
- **In a separate soul file:** Detailed personality backstory, extended guidelines, creative writing style, philosophical grounding — things the agent can reference when relevant but don't need to consume context tokens on every turn.
|
||||
|
||||
**File placement depends on Phase 1 choice:**
|
||||
- Shared personality → edit `groups/global/CLAUDE.md` for the core traits, create `groups/global/soul.md` for the extended content. All groups will see both.
|
||||
- Separate / main only → edit `groups/main/CLAUDE.md`, create `groups/main/soul.md`.
|
||||
|
||||
Add a reference in the relevant CLAUDE.md: "Your personality and extended behavioral guidelines are in `soul.md`. Refer to it for identity questions or when crafting responses that need your full character."
|
||||
|
||||
Show proposed edits to the user before applying. This is a thoughtful merge, not a copy-paste.
|
||||
|
||||
### USER.md
|
||||
|
||||
Read `<STATE_DIR>/workspace/USER.md` if it exists.
|
||||
|
||||
Create `groups/main/user-context.md` with the user information. Add a reference in CLAUDE.md: "Information about your user is in `user-context.md`. Read it when you need context about who you're talking to."
|
||||
|
||||
Ask if they want any critical user facts (name, timezone, key preferences) directly in CLAUDE.md for always-on awareness.
|
||||
|
||||
### MEMORY.md
|
||||
|
||||
Read `<STATE_DIR>/workspace/MEMORY.md` if it exists.
|
||||
|
||||
Show the contents and discuss what's worth keeping. Some memory entries may be stale or OpenClaw-specific. Create `groups/main/memories.md` for relevant items. Add a reference in CLAUDE.md.
|
||||
|
||||
### Daily memory files (`workspace/memory/*.md`)
|
||||
|
||||
If DAILY_MEMORY_FILES > 0 in the discovery output, OpenClaw accumulated dated memory files (e.g. `2024-01-01.md`). These contain observations, facts, and context gathered over time.
|
||||
|
||||
AskUserQuestion: "You have N daily memory files from OpenClaw. How would you like to handle them?"
|
||||
|
||||
1. **Copy as-is (recommended for many files)** — "I'll create a `daily-memories/` folder in your group directory and copy them over. Your agent can reference them when needed."
|
||||
- Create the folder in the appropriate group directory (per Phase 1 decision)
|
||||
- Copy all `.md` files: `cp -r <workspace>/memory/*.md <group_dir>/daily-memories/`
|
||||
- Add a reference in CLAUDE.md: "Historical daily memory files from your previous system are in `daily-memories/`. Refer to them when you need context about past events or observations."
|
||||
|
||||
2. **Consolidate into memories** — "I'll read through them, extract the durable facts, and add them to your memories file. This reduces clutter but takes longer."
|
||||
- Read each file, extract entries worth keeping (skip transient observations, focus on durable facts about the user, preferences, recurring topics)
|
||||
- Consolidate into `memories.md`
|
||||
- Use sub-agents for large volumes (>10 files)
|
||||
|
||||
3. **Skip** — "Don't bring daily memories over."
|
||||
|
||||
### OpenClaw Skills
|
||||
|
||||
If SKILL_COUNT > 0 in discovery, OpenClaw had custom skills. The SKILL.md format is a shared standard — skills are directly portable.
|
||||
|
||||
The discovery reports skill names and source locations. For each skill, read just the YAML front matter (name + description at the top of SKILL.md) and present a list to the user: skill name, description, source location. Let the user select which ones to bring over.
|
||||
|
||||
For confirmed skills, copy the entire skill directory as-is:
|
||||
|
||||
```bash
|
||||
cp -r <skill_source_dir> container/skills/<skill_name>
|
||||
```
|
||||
|
||||
After all skills are copied, a container rebuild is needed — note this for post-migration: `./container/build.sh`.
|
||||
|
||||
### Config-registered plugins and skills
|
||||
|
||||
If CONFIG_PLUGIN_COUNT > 0 in discovery, OpenClaw had installed plugins/skills with API keys (e.g. `plugins.entries.brave`, `skills.entries.openai-whisper-api`). These are functional tools the agent had access to.
|
||||
|
||||
For each detected plugin, present the name to the user and discuss whether to set it up in NanoClaw. Read the OpenClaw config section to understand what it is, then:
|
||||
|
||||
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. `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.
|
||||
|
||||
4. **If the plugin wraps an API** — discuss with the user what it did and offer to implement the equivalent: save the API key to `.env`, write a container skill with instructions for using the API, or wire it into the message flow if it's something automatic (e.g. voice transcription).
|
||||
|
||||
5. **If unclear** — discuss with the user what the plugin did and decide together. Don't install unknown packages or search for replacements — that's a supply chain risk.
|
||||
|
||||
For API keys, read the config value directly (don't display raw keys) and write to `.env`. The discovery script reports which plugins have keys but never extracts them.
|
||||
|
||||
### Other files (TOOLS.md, HEARTBEAT.md, BOOTSTRAP.md, AGENTS.md)
|
||||
|
||||
If these exist, briefly mention them and explain:
|
||||
- TOOLS.md: NanoClaw agents have their own tool discovery; this doesn't transfer
|
||||
- HEARTBEAT.md: NanoClaw uses scheduled tasks instead
|
||||
- BOOTSTRAP.md: NanoClaw uses CLAUDE.md and container skills instead
|
||||
- AGENTS.md: Already covered in the Phase 1 groups discussion
|
||||
|
||||
## Phase 4: Channel Credentials
|
||||
|
||||
For each channel found in the discovery results, handle it based on NanoClaw support:
|
||||
|
||||
### Supported channels (whatsapp, telegram, slack, discord)
|
||||
|
||||
Run the credential extraction script with `--write-env .env` so it writes credentials directly to NanoClaw's `.env` file. The script never emits raw credential values to stdout — only masked versions.
|
||||
|
||||
First, run without `--write-env` to preview:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
**If HAS_CREDENTIAL=false but the user expects a credential:** The extraction script may not recognize the config structure. Fall back to reading the channel section of `openclaw.json` directly with the Read tool and look for any field that contains a token or key value. Ask the user to confirm.
|
||||
|
||||
If HAS_CREDENTIAL=true: Show the masked credential (`CREDENTIAL_MASKED`). AskUserQuestion:
|
||||
1. **Use this credential** — run again with `--write-env .env` to save it
|
||||
2. **Enter a new one** — ask in plain text, write to `.env` manually
|
||||
3. **Skip this channel** — don't configure
|
||||
|
||||
If using the credential:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
**Credential destination note:** Credentials are saved to `.env` for now. During `/setup`, the credential step will either keep them in `.env` (Apple Container) or migrate them to the OneCLI vault (Docker). The user doesn't need to worry about this now.
|
||||
|
||||
For Slack: there are two credentials (bot token + app token). The script handles both in one run — check `HAS_CREDENTIAL_2` and `NANOCLAW_ENV_VAR_2` in the status block.
|
||||
|
||||
**WhatsApp special case:** WhatsApp uses QR/pairing-code authentication, not a token. Do not copy auth state from OpenClaw — encryption sessions become stale after copying and messages fail to decrypt. Authentication will be handled during `/setup` via the `/add-whatsapp` skill (takes about 60 seconds with a pairing code). Just note that WhatsApp was configured and move on.
|
||||
|
||||
**Allowlist note:** If the channel had `allowFrom` or group policies, these were already handled in Phase 2 (sender allowlists). Mention that the allowlist file was created earlier.
|
||||
|
||||
### Unsupported channels (signal, matrix, irc, msteams, feishu, etc.)
|
||||
|
||||
Explain briefly: "NanoClaw doesn't have a `<channel>` integration yet, but channels are added over time via skills. Any groups from this channel were already registered in Phase 1 — they'll activate when the channel is added."
|
||||
|
||||
If there are credentials (tokens, keys) for the unsupported channel, offer to save them to `.env` with a descriptive variable name (e.g. `SIGNAL_ACCOUNT`, `MATRIX_ACCESS_TOKEN`) so they're available when the channel is eventually supported.
|
||||
|
||||
Don't invoke channel skills here — just prepare `.env` credentials. Channel code is installed during `/setup`.
|
||||
|
||||
## Phase 5: Scheduled Tasks
|
||||
|
||||
Read `<STATE_DIR>/cron/jobs.json` with the Read tool. If the file doesn't exist or has no jobs, skip this phase.
|
||||
|
||||
If jobs exist, read `${CLAUDE_SKILL_DIR}/MIGRATE_CRONS.md` for the full OpenClaw cron format, NanoClaw table schema, field mapping, and SQL insert template. Follow those instructions for each job.
|
||||
|
||||
## Phase 6: Webhooks, MCP, and Other Config
|
||||
|
||||
Read relevant sections from `<STATE_DIR>/openclaw.json` directly with the Read tool. This phase is fully conversational.
|
||||
|
||||
### MCP Servers
|
||||
|
||||
If MCP_SERVERS was non-empty in discovery, these can be ported. Claude Code supports MCP servers natively. Read the OpenClaw config's `mcp.servers` section to get each server's details (`command`, `args`, `env`, `url`).
|
||||
|
||||
MCP servers in NanoClaw are registered in the agent-runner source code. Before editing, grep for `mcpServers` in `container/agent-runner/src/` to find the current location — it's expected to be in `index.ts` in the `query()` options, but may have moved. For each OpenClaw MCP server the user wants to bring over:
|
||||
|
||||
1. Read its config: command, args, env, url
|
||||
2. **stdio servers** (have `command`): Add an entry to the `mcpServers` object in `container/agent-runner/src/index.ts`. The command runs inside the container, so it needs to be available there (Node.js/npx-based servers work; custom binaries would need to be added to the Dockerfile).
|
||||
3. **HTTP/SSE servers** (have `url`): These work if the URL is accessible from inside the container. Add them the same way.
|
||||
4. **Environment variables**: Any `env` values that reference secrets should be added to `.env` and passed through via `process.env.*` in the mcpServers entry.
|
||||
|
||||
After adding all MCP servers, a container rebuild is needed: `./container/build.sh`
|
||||
|
||||
Show the user each server and ask which to bring over. For servers that need custom binaries not available in the container, note them for manual setup.
|
||||
|
||||
### Webhooks and Endpoints
|
||||
|
||||
If the config has webhook sections (in `cron.webhook`, `cron.failureDestination`, or channel-specific webhooks):
|
||||
- Explain what they were used for
|
||||
- These don't map directly but NanoClaw can be customized to support them
|
||||
- Discuss the use case with the user and propose a solution if it's important to them
|
||||
- For simple webhook notifications: a task script with `curl` often suffices
|
||||
|
||||
### Other Config
|
||||
|
||||
Scan the config for notable sections and briefly mention anything that doesn't carry over:
|
||||
- **Exec approvals / command allowlist:** NanoClaw uses container isolation instead — the agent runs with `--dangerously-skip-permissions` inside a sandboxed container
|
||||
- **Human delay:** Not applicable in NanoClaw's container model
|
||||
- **Compaction:** Handled by Claude Code SDK automatically
|
||||
- **TTS:** Not built into NanoClaw
|
||||
- **Model configuration:** NanoClaw uses whatever Anthropic model the credential provides access to
|
||||
|
||||
Don't belabor these — just mention and move on.
|
||||
|
||||
## Phase 7: Summary
|
||||
|
||||
### Summary
|
||||
|
||||
Print a comprehensive summary:
|
||||
|
||||
**Migrated:**
|
||||
- Assistant name → `.env` ASSISTANT_NAME + CLAUDE.md templates updated
|
||||
- Groups → registered in database, folders created with CLAUDE.md templates
|
||||
- Timezone → `.env` TZ
|
||||
- Anthropic credential → `.env` (for setup to pick up)
|
||||
- Sender allowlists → `~/.config/nanoclaw/sender-allowlist.json`
|
||||
- Personality → CLAUDE.md (core) + `soul.md` (extended), placed per Phase 1 decision (global or per-group)
|
||||
- User context → `user-context.md`
|
||||
- Memories → `memories.md` + daily memory files (copied to `daily-memories/` or consolidated)
|
||||
- OpenClaw skills → copied to `container/skills/`
|
||||
- Channel credentials → `.env` (list which channels)
|
||||
- Scheduled tasks → inserted into database or noted for post-setup
|
||||
- MCP servers → registered in agent-runner
|
||||
|
||||
**Noted for later:**
|
||||
- Channel code installation (happens during `/setup`)
|
||||
- Task creation (if deferred due to no registered group yet)
|
||||
- Container rebuild needed (if skills or MCP servers were added): `./container/build.sh`
|
||||
|
||||
**Not applicable:**
|
||||
- Unsupported channels (list them — groups registered for future)
|
||||
- OpenClaw-specific features (exec approvals, human delay, TTS, model config, session reset policies, etc.)
|
||||
|
||||
**Discussed and deferred:**
|
||||
- List any customizations agreed on but not yet implemented
|
||||
|
||||
Remind: "Run `/setup` next to complete your NanoClaw installation. Channel credentials are already prepared in `.env`. When setup asks which channels to enable, select the ones we configured."
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Config parse error:** If `openclaw.json` fails to parse, it may use JSON5 features the parser doesn't handle. Ask the user to check the file for unusual syntax. As a fallback, the agent can read the file directly and work with it manually.
|
||||
|
||||
**Credential not found:** If a channel credential resolves to empty, it may use `source:"exec"` or `source:"file"` SecretRef. These can't be auto-extracted. Ask the user to provide the value directly.
|
||||
|
||||
**Multi-agent complexity:** If the user had many agents with different configs, focus on the primary/default agent first. Additional agents can be set up as separate NanoClaw groups later.
|
||||
@@ -0,0 +1,734 @@
|
||||
/**
|
||||
* Discover an existing OpenClaw installation and emit a structured summary.
|
||||
*
|
||||
* 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,
|
||||
* checks cron jobs, MCP servers, and channel credentials.
|
||||
*
|
||||
* Emits a status block on stdout:
|
||||
* === NANOCLAW MIGRATE: DISCOVERY ===
|
||||
* ...
|
||||
* === END ===
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON5-tolerant parser (no dependency)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseJson5(text: string): unknown {
|
||||
// Strip single-line comments (// ...) that aren't inside strings
|
||||
let cleaned = text.replace(
|
||||
/("(?:[^"\\]|\\.)*")|\/\/[^\n]*/g,
|
||||
(match, str) => (str ? str : ''),
|
||||
);
|
||||
// Strip block comments (/* ... */)
|
||||
cleaned = cleaned.replace(
|
||||
/("(?:[^"\\]|\\.)*")|\/\*[\s\S]*?\*\//g,
|
||||
(match, str) => (str ? str : ''),
|
||||
);
|
||||
// Strip trailing commas before } or ]
|
||||
cleaned = cleaned.replace(/,\s*([}\]])/g, '$1');
|
||||
return JSON.parse(cleaned);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status block emitter (mirrors setup/status.ts convention)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function emitStatus(fields: Record<string, string | number | boolean>): void {
|
||||
const lines = ['=== NANOCLAW MIGRATE: DISCOVERY ==='];
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
lines.push(`${key}: ${value}`);
|
||||
}
|
||||
lines.push('=== END ===');
|
||||
console.log(lines.join('\n'));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI arg parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseArgs(): { stateDir?: string } {
|
||||
const args = process.argv.slice(2);
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--state-dir' && args[i + 1]) {
|
||||
return { stateDir: args[i + 1] };
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function resolveStateDir(explicit?: string): string | null {
|
||||
const home = os.homedir();
|
||||
const candidates: string[] = [];
|
||||
|
||||
if (explicit) {
|
||||
// Expand ~ prefix
|
||||
const expanded = explicit.startsWith('~')
|
||||
? path.join(home, explicit.slice(1))
|
||||
: explicit;
|
||||
candidates.push(expanded);
|
||||
}
|
||||
|
||||
if (process.env.OPENCLAW_STATE_DIR) {
|
||||
candidates.push(process.env.OPENCLAW_STATE_DIR);
|
||||
}
|
||||
|
||||
candidates.push(path.join(home, '.openclaw'));
|
||||
candidates.push(path.join(home, '.clawdbot'));
|
||||
|
||||
for (const dir of candidates) {
|
||||
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function loadConfig(
|
||||
stateDir: string,
|
||||
): Record<string, unknown> | null {
|
||||
for (const name of ['openclaw.json', 'clawdbot.json']) {
|
||||
const configPath = path.join(stateDir, name);
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath, 'utf-8');
|
||||
return parseJson5(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
// Try next name
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Channel detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ChannelInfo {
|
||||
name: string;
|
||||
hasCreds: boolean;
|
||||
}
|
||||
|
||||
const SUPPORTED_CHANNELS = new Set([
|
||||
'whatsapp',
|
||||
'telegram',
|
||||
'slack',
|
||||
'discord',
|
||||
]);
|
||||
|
||||
// Fields that indicate a credential is present for each channel
|
||||
const CREDENTIAL_FIELDS: Record<string, string[]> = {
|
||||
telegram: ['botToken'],
|
||||
discord: ['token'],
|
||||
slack: ['botToken', 'appToken'],
|
||||
whatsapp: [], // Auth-state based, no token
|
||||
signal: ['account'],
|
||||
imessage: [],
|
||||
matrix: ['homeserverUrl', 'accessToken'],
|
||||
irc: ['server'],
|
||||
msteams: ['appId'],
|
||||
feishu: ['appId'],
|
||||
googlechat: [],
|
||||
mattermost: ['token', 'url'],
|
||||
zalo: [],
|
||||
bluebubbles: ['url'],
|
||||
};
|
||||
|
||||
const ALL_KNOWN_CHANNELS = new Set([
|
||||
'whatsapp', 'telegram', 'slack', 'discord', 'signal',
|
||||
'imessage', 'matrix', 'irc', 'msteams', 'feishu',
|
||||
'googlechat', 'mattermost', 'zalo', 'bluebubbles',
|
||||
]);
|
||||
|
||||
function detectChannels(
|
||||
config: Record<string, unknown>,
|
||||
): ChannelInfo[] {
|
||||
// Check both config.channels.* (newer) and top-level config.* (older/legacy)
|
||||
const channelsSections: Record<string, unknown> = {};
|
||||
|
||||
// Source 1: channels.* (standard location)
|
||||
const nested = config.channels as Record<string, unknown> | undefined;
|
||||
if (nested) {
|
||||
for (const [k, v] of Object.entries(nested)) {
|
||||
if (v && typeof v === 'object') channelsSections[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
// Source 2: top-level keys matching known channel names (legacy format)
|
||||
for (const key of Object.keys(config)) {
|
||||
if (ALL_KNOWN_CHANNELS.has(key) && !channelsSections[key]) {
|
||||
const v = config[key];
|
||||
if (v && typeof v === 'object') channelsSections[key] = v;
|
||||
}
|
||||
}
|
||||
|
||||
const results: ChannelInfo[] = [];
|
||||
|
||||
for (const [name, section] of Object.entries(channelsSections)) {
|
||||
if (!section || typeof section !== 'object') continue;
|
||||
const ch = section as Record<string, unknown>;
|
||||
|
||||
// Check if any credential field is present and non-empty
|
||||
const credFields = CREDENTIAL_FIELDS[name] ?? [];
|
||||
let hasCreds = false;
|
||||
|
||||
for (const field of credFields) {
|
||||
const val = ch[field];
|
||||
if (val && (typeof val === 'string' || typeof val === 'object')) {
|
||||
hasCreds = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check accounts for multi-account setups
|
||||
if (!hasCreds && ch.accounts && typeof ch.accounts === 'object') {
|
||||
for (const acct of Object.values(
|
||||
ch.accounts as Record<string, unknown>,
|
||||
)) {
|
||||
if (!acct || typeof acct !== 'object') continue;
|
||||
const a = acct as Record<string, unknown>;
|
||||
for (const field of credFields) {
|
||||
if (
|
||||
a[field] &&
|
||||
(typeof a[field] === 'string' || typeof a[field] === 'object')
|
||||
) {
|
||||
hasCreds = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasCreds) break;
|
||||
}
|
||||
}
|
||||
|
||||
// WhatsApp: check for auth state directory instead of token
|
||||
if (name === 'whatsapp' && !hasCreds) {
|
||||
// Will be checked separately via agents directory
|
||||
hasCreds = false;
|
||||
}
|
||||
|
||||
results.push({ name, hasCreds });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workspace scanning
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const WORKSPACE_FILES = [
|
||||
'SOUL.md',
|
||||
'USER.md',
|
||||
'MEMORY.md',
|
||||
'IDENTITY.md',
|
||||
'TOOLS.md',
|
||||
'HEARTBEAT.md',
|
||||
'BOOTSTRAP.md',
|
||||
'AGENTS.md',
|
||||
];
|
||||
|
||||
function findWorkspace(stateDir: string, config: Record<string, unknown> | null): {
|
||||
dir: string | null;
|
||||
files: string[];
|
||||
} {
|
||||
// Check config-specified workspace path first (agent.workspace or agents.defaults.workspace)
|
||||
const configPaths: string[] = [];
|
||||
if (config) {
|
||||
const agentWs = (config.agent as Record<string, unknown> | undefined)?.workspace as string | undefined;
|
||||
if (agentWs) configPaths.push(agentWs.startsWith('~') ? path.join(os.homedir(), agentWs.slice(1)) : agentWs);
|
||||
const defaultsWs = ((config.agents as Record<string, unknown> | undefined)?.defaults as Record<string, unknown> | undefined)?.workspace as string | undefined;
|
||||
if (defaultsWs) configPaths.push(defaultsWs.startsWith('~') ? path.join(os.homedir(), defaultsWs.slice(1)) : defaultsWs);
|
||||
}
|
||||
|
||||
// Check config-specified paths, then default locations
|
||||
const candidates = [
|
||||
...configPaths,
|
||||
...['workspace', 'workspace.default'].map((n) => path.join(stateDir, n)),
|
||||
];
|
||||
|
||||
for (const ws of candidates) {
|
||||
if (fs.existsSync(ws) && fs.statSync(ws).isDirectory()) {
|
||||
const found = WORKSPACE_FILES.filter((f) =>
|
||||
fs.existsSync(path.join(ws, f)),
|
||||
);
|
||||
if (found.length > 0) {
|
||||
return { dir: ws, files: found };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check agent-specific workspaces
|
||||
const agentsDir = path.join(stateDir, 'agents');
|
||||
if (fs.existsSync(agentsDir)) {
|
||||
for (const agentId of fs.readdirSync(agentsDir)) {
|
||||
for (const wsName of ['workspace', 'workspace.default']) {
|
||||
const ws = path.join(agentsDir, agentId, wsName);
|
||||
if (fs.existsSync(ws) && fs.statSync(ws).isDirectory()) {
|
||||
const found = WORKSPACE_FILES.filter((f) =>
|
||||
fs.existsSync(path.join(ws, f)),
|
||||
);
|
||||
if (found.length > 0) {
|
||||
return { dir: ws, files: found };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { dir: null, files: [] };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Daily memory file detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function countDailyMemoryFiles(workspaceDir: string | null): number {
|
||||
if (!workspaceDir) return 0;
|
||||
const memoryDir = path.join(workspaceDir, 'memory');
|
||||
if (!fs.existsSync(memoryDir) || !fs.statSync(memoryDir).isDirectory()) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return fs
|
||||
.readdirSync(memoryDir)
|
||||
.filter((f) => f.endsWith('.md'))
|
||||
.length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Skills detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface SkillInfo {
|
||||
name: string;
|
||||
source: string; // 'workspace' | 'shared' | 'personal' | 'project'
|
||||
path: string;
|
||||
}
|
||||
|
||||
function detectSkills(
|
||||
stateDir: string,
|
||||
workspaceDir: string | null,
|
||||
): SkillInfo[] {
|
||||
const skills: SkillInfo[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const scanDir = (dir: string, source: string) => {
|
||||
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return;
|
||||
try {
|
||||
for (const entry of fs.readdirSync(dir)) {
|
||||
const skillDir = path.join(dir, entry);
|
||||
if (!fs.statSync(skillDir).isDirectory()) continue;
|
||||
// A directory is a skill if it contains SKILL.md
|
||||
if (fs.existsSync(path.join(skillDir, 'SKILL.md'))) {
|
||||
if (seen.has(entry)) continue;
|
||||
seen.add(entry);
|
||||
skills.push({ name: entry, source, path: skillDir });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore read errors
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Workspace skills
|
||||
if (workspaceDir) {
|
||||
scanDir(path.join(workspaceDir, 'skills'), 'workspace');
|
||||
// 4. Project-level shared skills
|
||||
scanDir(path.join(workspaceDir, '.agents', 'skills'), 'project');
|
||||
}
|
||||
|
||||
// 2. Managed/shared skills
|
||||
scanDir(path.join(stateDir, 'skills'), 'shared');
|
||||
|
||||
// 3. Personal cross-project skills
|
||||
const personalSkills = path.join(os.homedir(), '.agents', 'skills');
|
||||
scanDir(personalSkills, 'personal');
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Identity extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractIdentityName(stateDir: string, workspaceDir: string | null): string {
|
||||
if (!workspaceDir) return '';
|
||||
|
||||
const identityPath = path.join(workspaceDir, 'IDENTITY.md');
|
||||
if (!fs.existsSync(identityPath)) return '';
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(identityPath, 'utf-8');
|
||||
// IDENTITY.md uses key:value format, e.g. "name: Claw"
|
||||
const match = content.match(/^name:\s*(.+)/im);
|
||||
return match ? match[1].trim() : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function detectAgents(stateDir: string): string[] {
|
||||
const agentsDir = path.join(stateDir, 'agents');
|
||||
if (!fs.existsSync(agentsDir)) return [];
|
||||
|
||||
try {
|
||||
return fs
|
||||
.readdirSync(agentsDir)
|
||||
.filter((f) => {
|
||||
const p = path.join(agentsDir, f);
|
||||
return fs.statSync(p).isDirectory() && !f.startsWith('.');
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Group detection — from session store and channel config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface GroupInfo {
|
||||
channel: string;
|
||||
id: string; // Platform-specific ID (WhatsApp JID, Telegram chat ID, etc.)
|
||||
name: string;
|
||||
source: 'session' | 'config';
|
||||
}
|
||||
|
||||
/**
|
||||
* Map OpenClaw session key channel:kind:id to NanoClaw JID format.
|
||||
* OpenClaw keys: "whatsapp:group:120...@g.us", "telegram:group:-10012345"
|
||||
* NanoClaw JIDs: "120...@g.us", "tg:-10012345", "dc:12345", "slack:C12345"
|
||||
*/
|
||||
function toNanoClawJid(channel: string, id: string): string {
|
||||
switch (channel) {
|
||||
case 'whatsapp':
|
||||
return id; // Already in JID format (120...@g.us)
|
||||
case 'telegram':
|
||||
return `tg:${id}`;
|
||||
case 'discord':
|
||||
return `dc:${id}`;
|
||||
case 'slack':
|
||||
return `slack:${id}`;
|
||||
default:
|
||||
return `${channel}:${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
function detectGroups(
|
||||
stateDir: string,
|
||||
config: Record<string, unknown> | null,
|
||||
agents: string[],
|
||||
): GroupInfo[] {
|
||||
const groups: GroupInfo[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// Source 1: Session store — scan for group session keys
|
||||
for (const agentId of agents) {
|
||||
const sessionsPath = path.join(
|
||||
stateDir,
|
||||
'agents',
|
||||
agentId,
|
||||
'sessions',
|
||||
'sessions.json',
|
||||
);
|
||||
if (!fs.existsSync(sessionsPath)) continue;
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(sessionsPath, 'utf-8');
|
||||
const data = JSON.parse(raw) as Record<string, unknown>;
|
||||
|
||||
// Sessions can be stored as an object with session keys, or as
|
||||
// { sessions: { key: entry } } or { entries: [...] }
|
||||
const entries =
|
||||
(data.sessions as Record<string, unknown>) ??
|
||||
(data.entries as Record<string, unknown>) ??
|
||||
data;
|
||||
|
||||
for (const [key, value] of Object.entries(entries)) {
|
||||
// Match session keys like "whatsapp:group:120...@g.us"
|
||||
// or prefixed "agent:main:whatsapp:group:120...@g.us"
|
||||
// Also match DM sessions: "whatsapp:dm:number@s.whatsapp.net"
|
||||
const match = key.match(/(\w+):(group|dm|channel):(.+)$/i);
|
||||
if (!match) continue;
|
||||
|
||||
const [, channel, kind, id] = match;
|
||||
// Skip DM sessions for group detection — they're individual chats
|
||||
if (kind === 'dm') continue;
|
||||
const dedupKey = `${channel}:${id}`;
|
||||
if (seen.has(dedupKey)) continue;
|
||||
seen.add(dedupKey);
|
||||
|
||||
// Try to extract display name from session entry
|
||||
let name = '';
|
||||
if (value && typeof value === 'object') {
|
||||
const entry = value as Record<string, unknown>;
|
||||
name =
|
||||
(entry.displayName as string) ??
|
||||
(entry.label as string) ??
|
||||
(entry.subject as string) ??
|
||||
'';
|
||||
}
|
||||
|
||||
groups.push({
|
||||
channel,
|
||||
id,
|
||||
name: name || id,
|
||||
source: 'session',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// Source 2: Channel config — groups explicitly configured
|
||||
if (config) {
|
||||
const channels =
|
||||
(config.channels as Record<string, unknown> | undefined) ?? {};
|
||||
for (const [channelName, channelSection] of Object.entries(channels)) {
|
||||
if (!channelSection || typeof channelSection !== 'object') continue;
|
||||
const ch = channelSection as Record<string, unknown>;
|
||||
|
||||
// WhatsApp/Telegram: channels.<channel>.groups.<groupId>
|
||||
const configGroups = ch.groups as Record<string, unknown> | undefined;
|
||||
if (configGroups) {
|
||||
for (const groupId of Object.keys(configGroups)) {
|
||||
const dedupKey = `${channelName}:${groupId}`;
|
||||
if (seen.has(dedupKey)) continue;
|
||||
seen.add(dedupKey);
|
||||
groups.push({
|
||||
channel: channelName,
|
||||
id: groupId,
|
||||
name: groupId,
|
||||
source: 'config',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Discord: channels.discord.guilds.<guildId>
|
||||
if (channelName === 'discord') {
|
||||
const guilds = ch.guilds as Record<string, unknown> | undefined;
|
||||
if (guilds) {
|
||||
for (const guildId of Object.keys(guilds)) {
|
||||
const dedupKey = `discord:${guildId}`;
|
||||
if (seen.has(dedupKey)) continue;
|
||||
seen.add(dedupKey);
|
||||
groups.push({
|
||||
channel: 'discord',
|
||||
id: guildId,
|
||||
name: guildId,
|
||||
source: 'config',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cron job counting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function countCronJobs(stateDir: string): {
|
||||
count: number;
|
||||
summaries: string[];
|
||||
} {
|
||||
const jobsPath = path.join(stateDir, 'cron', 'jobs.json');
|
||||
if (!fs.existsSync(jobsPath)) return { count: 0, summaries: [] };
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(jobsPath, 'utf-8');
|
||||
const data = JSON.parse(raw) as {
|
||||
jobs?: Array<{ name?: string; enabled?: boolean }>;
|
||||
};
|
||||
const jobs = data.jobs ?? [];
|
||||
const summaries = jobs
|
||||
.filter((j) => j.enabled !== false)
|
||||
.map((j) => j.name || 'unnamed')
|
||||
.slice(0, 10);
|
||||
return { count: jobs.length, summaries };
|
||||
} catch {
|
||||
return { count: 0, summaries: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config-registered plugins and skills (with API keys)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ConfigPlugin {
|
||||
name: string;
|
||||
source: 'skills.entries' | 'plugins.entries';
|
||||
hasApiKey: boolean;
|
||||
}
|
||||
|
||||
function detectConfigPlugins(
|
||||
config: Record<string, unknown>,
|
||||
): ConfigPlugin[] {
|
||||
const results: ConfigPlugin[] = [];
|
||||
|
||||
// Check skills.entries (e.g. openai-whisper-api with apiKey)
|
||||
const skills = config.skills as Record<string, unknown> | undefined;
|
||||
const skillEntries = skills?.entries as Record<string, unknown> | undefined;
|
||||
if (skillEntries) {
|
||||
for (const [name, entry] of Object.entries(skillEntries)) {
|
||||
if (!entry || typeof entry !== 'object') continue;
|
||||
const e = entry as Record<string, unknown>;
|
||||
const hasKey = !!(e.apiKey || e.token || e.key);
|
||||
results.push({ name, source: 'skills.entries', hasApiKey: hasKey });
|
||||
}
|
||||
}
|
||||
|
||||
// Check plugins.entries (e.g. brave with config.webSearch.apiKey)
|
||||
const plugins = config.plugins as Record<string, unknown> | undefined;
|
||||
const pluginEntries = plugins?.entries as Record<string, unknown> | undefined;
|
||||
if (pluginEntries) {
|
||||
for (const [name, entry] of Object.entries(pluginEntries)) {
|
||||
if (!entry || typeof entry !== 'object') continue;
|
||||
// Deep-search for apiKey in nested config
|
||||
const hasKey = JSON.stringify(entry).includes('apiKey');
|
||||
results.push({ name, source: 'plugins.entries', hasApiKey: hasKey });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MCP server detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function detectMcpServers(
|
||||
config: Record<string, unknown>,
|
||||
): string[] {
|
||||
const mcp = config.mcp as Record<string, unknown> | undefined;
|
||||
if (!mcp) return [];
|
||||
const servers = mcp.servers as Record<string, unknown> | undefined;
|
||||
if (!servers) return [];
|
||||
return Object.keys(servers);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function main(): void {
|
||||
const { stateDir: explicitDir } = parseArgs();
|
||||
const stateDir = resolveStateDir(explicitDir);
|
||||
|
||||
if (!stateDir) {
|
||||
emitStatus({ STATUS: 'not_found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const config = loadConfig(stateDir);
|
||||
const channels = config ? detectChannels(config) : [];
|
||||
const { dir: workspaceDir, files: workspaceFiles } =
|
||||
findWorkspace(stateDir, config);
|
||||
const identityName = extractIdentityName(stateDir, workspaceDir);
|
||||
const agents = detectAgents(stateDir);
|
||||
const groups = detectGroups(stateDir, config, agents);
|
||||
const { count: cronCount, summaries: cronSummaries } =
|
||||
countCronJobs(stateDir);
|
||||
const mcpServers = config ? detectMcpServers(config) : [];
|
||||
const dailyMemoryFiles = countDailyMemoryFiles(workspaceDir);
|
||||
const skills = detectSkills(stateDir, workspaceDir);
|
||||
const configPlugins = config ? detectConfigPlugins(config) : [];
|
||||
|
||||
// Format channels as "name(has_creds)" or "name(no_creds)"
|
||||
const channelList = channels
|
||||
.map((c) => `${c.name}(${c.hasCreds ? 'has_creds' : 'no_creds'})`)
|
||||
.join(',');
|
||||
|
||||
// Separate supported vs unsupported
|
||||
const unsupported = channels
|
||||
.filter((c) => !SUPPORTED_CHANNELS.has(c.name))
|
||||
.map((c) => c.name)
|
||||
.join(',');
|
||||
|
||||
// Format groups as "channel:id(name)" — also include NanoClaw JID mapping
|
||||
const groupList = groups
|
||||
.map(
|
||||
(g) =>
|
||||
`${g.channel}:${g.id}(${g.name})=>${toNanoClawJid(g.channel, g.id)}`,
|
||||
)
|
||||
.join('|');
|
||||
|
||||
// Format skills as "name(source)" list
|
||||
const skillList = skills
|
||||
.map((s) => `${s.name}(${s.source})`)
|
||||
.join(',');
|
||||
|
||||
// Dump raw top-level config keys so Claude can see what exists
|
||||
// beyond what this script specifically detects
|
||||
const configTopKeys = config ? Object.keys(config).sort().join(',') : 'none';
|
||||
const configChannelKeys = config?.channels
|
||||
? Object.keys(config.channels as Record<string, unknown>).sort().join(',')
|
||||
: 'none';
|
||||
|
||||
// List files/dirs at the state dir root for manual inspection
|
||||
let stateDirContents = 'unknown';
|
||||
try {
|
||||
stateDirContents = fs
|
||||
.readdirSync(stateDir)
|
||||
.filter((f) => !f.startsWith('.'))
|
||||
.sort()
|
||||
.join(',');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
emitStatus({
|
||||
STATUS: 'found',
|
||||
STATE_DIR: stateDir,
|
||||
CONFIG_FOUND: config !== null,
|
||||
CONFIG_TOP_KEYS: configTopKeys,
|
||||
CONFIG_CHANNEL_KEYS: configChannelKeys,
|
||||
STATE_DIR_CONTENTS: stateDirContents,
|
||||
CHANNELS: channelList || 'none',
|
||||
UNSUPPORTED_CHANNELS: unsupported || 'none',
|
||||
WORKSPACE_DIR: workspaceDir || 'not_found',
|
||||
WORKSPACE_FILES: workspaceFiles.join(',') || 'none',
|
||||
IDENTITY_NAME: identityName || 'unknown',
|
||||
AGENT_COUNT: agents.length,
|
||||
AGENT_IDS: agents.join(',') || 'none',
|
||||
GROUPS: groupList || 'none',
|
||||
GROUP_COUNT: groups.length,
|
||||
DAILY_MEMORY_FILES: dailyMemoryFiles,
|
||||
SKILL_COUNT: skills.length,
|
||||
SKILLS: skillList || 'none',
|
||||
CONFIG_PLUGINS: configPlugins.map((p) => `${p.name}(${p.source}${p.hasApiKey ? ',has_key' : ''})`).join(',') || 'none',
|
||||
CONFIG_PLUGIN_COUNT: configPlugins.length,
|
||||
CRON_JOBS: cronCount,
|
||||
CRON_SUMMARIES: cronSummaries.join('|') || 'none',
|
||||
MCP_SERVERS: mcpServers.join(',') || 'none',
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* Extract a channel credential from an OpenClaw configuration and write it
|
||||
* directly to the NanoClaw .env file.
|
||||
*
|
||||
* 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:
|
||||
* - Plain string: "bot-token-value"
|
||||
* - Env template: "${TELEGRAM_BOT_TOKEN}"
|
||||
* - SecretRef object: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN" }
|
||||
*
|
||||
* Also reads <state-dir>/.env for env-based secrets.
|
||||
*
|
||||
* Credential values are NEVER emitted to stdout — only masked versions.
|
||||
* When --write-env is provided, the script writes credentials directly to
|
||||
* the target .env file so the agent never sees raw secrets.
|
||||
*
|
||||
* Emits a status block on stdout:
|
||||
* === NANOCLAW MIGRATE: CREDENTIAL ===
|
||||
* ...
|
||||
* === END ===
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON5-tolerant parser (same as discover script)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseJson5(text: string): unknown {
|
||||
let cleaned = text.replace(
|
||||
/("(?:[^"\\]|\\.)*")|\/\/[^\n]*/g,
|
||||
(match, str) => (str ? str : ''),
|
||||
);
|
||||
cleaned = cleaned.replace(
|
||||
/("(?:[^"\\]|\\.)*")|\/\*[\s\S]*?\*\//g,
|
||||
(match, str) => (str ? str : ''),
|
||||
);
|
||||
cleaned = cleaned.replace(/,\s*([}\]])/g, '$1');
|
||||
return JSON.parse(cleaned);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inline dotenv parser (reads key=value, skips comments)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseDotenv(filePath: string): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
if (!fs.existsSync(filePath)) return env;
|
||||
|
||||
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eqIdx = trimmed.indexOf('=');
|
||||
if (eqIdx === -1) continue;
|
||||
const key = trimmed.slice(0, eqIdx).trim();
|
||||
let value = trimmed.slice(eqIdx + 1).trim();
|
||||
// Strip surrounding quotes
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
env[key] = value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status block emitter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function emitStatus(fields: Record<string, string | number | boolean>): void {
|
||||
const lines = ['=== NANOCLAW MIGRATE: CREDENTIAL ==='];
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
lines.push(`${key}: ${value}`);
|
||||
}
|
||||
lines.push('=== END ===');
|
||||
console.log(lines.join('\n'));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Credential masking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function maskCredential(value: string): string {
|
||||
if (value.length < 10) return '****';
|
||||
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SecretRef resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface SecretRef {
|
||||
source: string;
|
||||
provider?: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function resolveSecretInput(
|
||||
value: unknown,
|
||||
dotenvVars: Record<string, string>,
|
||||
): { resolved: string | null; source: string; note?: string } {
|
||||
if (!value) return { resolved: null, source: 'missing' };
|
||||
|
||||
// Plain string
|
||||
if (typeof value === 'string') {
|
||||
// Check for env template: "${VAR_NAME}"
|
||||
const envMatch = value.match(/^\$\{([^}]+)\}$/);
|
||||
if (envMatch) {
|
||||
const envKey = envMatch[1];
|
||||
const envVal =
|
||||
dotenvVars[envKey] ?? process.env[envKey] ?? null;
|
||||
if (envVal) {
|
||||
return { resolved: envVal, source: 'env_template' };
|
||||
}
|
||||
return {
|
||||
resolved: null,
|
||||
source: 'env_template',
|
||||
note: `Environment variable ${envKey} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
// Plain literal value
|
||||
return { resolved: value, source: 'plain' };
|
||||
}
|
||||
|
||||
// SecretRef object
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const ref = value as SecretRef;
|
||||
if (ref.source === 'env') {
|
||||
const envVal =
|
||||
dotenvVars[ref.id] ?? process.env[ref.id] ?? null;
|
||||
if (envVal) {
|
||||
return { resolved: envVal, source: 'env_ref' };
|
||||
}
|
||||
return {
|
||||
resolved: null,
|
||||
source: 'env_ref',
|
||||
note: `Environment variable ${ref.id} not found`,
|
||||
};
|
||||
}
|
||||
if (ref.source === 'file') {
|
||||
return {
|
||||
resolved: null,
|
||||
source: 'file_ref',
|
||||
note: `File-based secret (${ref.id}) — cannot auto-extract, add manually`,
|
||||
};
|
||||
}
|
||||
if (ref.source === 'exec') {
|
||||
return {
|
||||
resolved: null,
|
||||
source: 'exec_ref',
|
||||
note: `Exec-based secret (${ref.id}) — cannot auto-extract, add manually`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { resolved: null, source: 'unknown' };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Channel credential mapping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ChannelCredentialSpec {
|
||||
// Fields to look for in the channel config
|
||||
fields: string[];
|
||||
// Corresponding NanoClaw env var names
|
||||
envVars: string[];
|
||||
}
|
||||
|
||||
const CHANNEL_SPECS: Record<string, ChannelCredentialSpec> = {
|
||||
telegram: {
|
||||
fields: ['botToken'],
|
||||
envVars: ['TELEGRAM_BOT_TOKEN'],
|
||||
},
|
||||
discord: {
|
||||
fields: ['token'],
|
||||
envVars: ['DISCORD_BOT_TOKEN'],
|
||||
},
|
||||
slack: {
|
||||
fields: ['botToken', 'appToken'],
|
||||
envVars: ['SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN'],
|
||||
},
|
||||
whatsapp: {
|
||||
fields: [], // Auth-state based, no token field
|
||||
envVars: [],
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI arg parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseArgs(): { channel: string; stateDir: string; writeEnv: string } {
|
||||
const args = process.argv.slice(2);
|
||||
let channel = '';
|
||||
let stateDir = '';
|
||||
let writeEnv = '';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--channel' && args[i + 1]) {
|
||||
channel = args[++i].toLowerCase();
|
||||
}
|
||||
if (args[i] === '--state-dir' && args[i + 1]) {
|
||||
stateDir = args[++i];
|
||||
}
|
||||
if (args[i] === '--write-env' && args[i + 1]) {
|
||||
writeEnv = args[++i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
console.error('Usage: --channel <name> --state-dir <path> [--write-env <path>]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Expand ~ prefix
|
||||
if (stateDir.startsWith('~')) {
|
||||
stateDir = path.join(os.homedir(), stateDir.slice(1));
|
||||
}
|
||||
|
||||
// Default state dir
|
||||
if (!stateDir) {
|
||||
const home = os.homedir();
|
||||
if (fs.existsSync(path.join(home, '.openclaw'))) {
|
||||
stateDir = path.join(home, '.openclaw');
|
||||
} else if (fs.existsSync(path.join(home, '.clawdbot'))) {
|
||||
stateDir = path.join(home, '.clawdbot');
|
||||
} else {
|
||||
console.error(
|
||||
'No OpenClaw directory found. Use --state-dir to specify.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return { channel, stateDir, writeEnv };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// .env writer — appends or replaces a KEY=VALUE line
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function writeEnvVar(envPath: string, key: string, value: string): void {
|
||||
let content = '';
|
||||
if (fs.existsSync(envPath)) {
|
||||
content = fs.readFileSync(envPath, 'utf-8');
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`^${key}=.*$`, 'm');
|
||||
const line = `${key}="${value}"`;
|
||||
|
||||
if (pattern.test(content)) {
|
||||
content = content.replace(pattern, line);
|
||||
} else {
|
||||
content = content.trimEnd() + (content ? '\n' : '') + line + '\n';
|
||||
}
|
||||
|
||||
fs.writeFileSync(envPath, content);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function main(): void {
|
||||
const { channel, stateDir, writeEnv } = parseArgs();
|
||||
const spec = CHANNEL_SPECS[channel];
|
||||
|
||||
// Load dotenv from state dir
|
||||
const dotenvVars = parseDotenv(path.join(stateDir, '.env'));
|
||||
|
||||
// Also check auth-profiles.json for API keys
|
||||
const authProfilesPath = path.join(stateDir, 'auth-profiles.json');
|
||||
let authProfiles: Record<string, unknown> = {};
|
||||
if (fs.existsSync(authProfilesPath)) {
|
||||
try {
|
||||
authProfiles = JSON.parse(
|
||||
fs.readFileSync(authProfilesPath, 'utf-8'),
|
||||
) as Record<string, unknown>;
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// WhatsApp special case: no token, auth-state based.
|
||||
// OpenClaw stores Baileys auth at <stateDir>/credentials/whatsapp/<accountId>/
|
||||
// using useMultiFileAuthState (same as NanoClaw). The files are directly compatible.
|
||||
if (channel === 'whatsapp') {
|
||||
const authPaths = [
|
||||
path.join(stateDir, 'credentials', 'whatsapp', 'default'),
|
||||
path.join(stateDir, 'credentials', 'whatsapp'),
|
||||
path.join(stateDir, 'wa-auth'),
|
||||
];
|
||||
|
||||
// Also scan credentials/whatsapp/ for any account subdirectory
|
||||
const waCredsDir = path.join(stateDir, 'credentials', 'whatsapp');
|
||||
if (fs.existsSync(waCredsDir)) {
|
||||
try {
|
||||
for (const entry of fs.readdirSync(waCredsDir)) {
|
||||
const candidate = path.join(waCredsDir, entry);
|
||||
if (fs.statSync(candidate).isDirectory()) {
|
||||
authPaths.push(candidate);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
let authStatePath = '';
|
||||
for (const p of authPaths) {
|
||||
// Look for creds.json inside the directory — that confirms valid Baileys auth state
|
||||
if (fs.existsSync(path.join(p, 'creds.json'))) {
|
||||
authStatePath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emitStatus({
|
||||
CHANNEL: 'whatsapp',
|
||||
HAS_CREDENTIAL: false,
|
||||
CREDENTIAL_SOURCE: 'auth_state',
|
||||
NOTE: authStatePath
|
||||
? `Baileys auth state found at ${authStatePath}. May not be portable across versions — recommend re-authenticating.`
|
||||
: 'No WhatsApp auth state found. Will need to authenticate during setup.',
|
||||
AUTH_STATE_PATH: authStatePath || 'not_found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown channel
|
||||
if (!spec) {
|
||||
emitStatus({
|
||||
CHANNEL: channel,
|
||||
HAS_CREDENTIAL: false,
|
||||
NOTE: `Channel "${channel}" is not supported by NanoClaw. Supported: telegram, discord, slack, whatsapp.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Load OpenClaw config
|
||||
let config: Record<string, unknown> | null = null;
|
||||
for (const name of ['openclaw.json', 'clawdbot.json']) {
|
||||
const configPath = path.join(stateDir, name);
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
config = parseJson5(
|
||||
fs.readFileSync(configPath, 'utf-8'),
|
||||
) as Record<string, unknown>;
|
||||
break;
|
||||
} catch {
|
||||
// Try next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
emitStatus({
|
||||
CHANNEL: channel,
|
||||
HAS_CREDENTIAL: false,
|
||||
NOTE: 'Could not load openclaw.json',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const channels =
|
||||
(config.channels as Record<string, unknown> | undefined) ?? {};
|
||||
const channelConfig =
|
||||
(channels[channel] as Record<string, unknown> | undefined) ?? {};
|
||||
|
||||
// Try to resolve each credential field
|
||||
const results: Array<{
|
||||
envVar: string;
|
||||
resolved: string | null;
|
||||
masked: string;
|
||||
source: string;
|
||||
note?: string;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < spec.fields.length; i++) {
|
||||
const field = spec.fields[i];
|
||||
const envVar = spec.envVars[i];
|
||||
|
||||
// Check top-level channel config first
|
||||
let rawValue = channelConfig[field];
|
||||
|
||||
// If not found, check first account
|
||||
if (!rawValue && channelConfig.accounts) {
|
||||
const accounts = channelConfig.accounts as Record<string, unknown>;
|
||||
const firstAccount = Object.values(accounts)[0] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (firstAccount) {
|
||||
rawValue = firstAccount[field];
|
||||
}
|
||||
}
|
||||
|
||||
const { resolved, source, note } = resolveSecretInput(
|
||||
rawValue,
|
||||
dotenvVars,
|
||||
);
|
||||
results.push({
|
||||
envVar,
|
||||
resolved,
|
||||
masked: resolved ? maskCredential(resolved) : '',
|
||||
source,
|
||||
note,
|
||||
});
|
||||
}
|
||||
|
||||
// Emit results for the primary credential
|
||||
const primary = results[0];
|
||||
if (!primary) {
|
||||
emitStatus({
|
||||
CHANNEL: channel,
|
||||
HAS_CREDENTIAL: false,
|
||||
NOTE: `No credential fields defined for ${channel}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If --write-env is set and credentials were resolved, write directly to .env.
|
||||
// Credential values never appear in stdout.
|
||||
let written = 0;
|
||||
if (writeEnv) {
|
||||
for (const r of results) {
|
||||
if (r.resolved) {
|
||||
writeEnvVar(writeEnv, r.envVar, r.resolved);
|
||||
written++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fields: Record<string, string | number | boolean> = {
|
||||
CHANNEL: channel,
|
||||
HAS_CREDENTIAL: !!primary.resolved,
|
||||
CREDENTIAL_SOURCE: primary.source,
|
||||
CREDENTIAL_MASKED: primary.masked || 'none',
|
||||
NANOCLAW_ENV_VAR: primary.envVar,
|
||||
};
|
||||
|
||||
if (writeEnv && written > 0) {
|
||||
fields.WRITTEN_TO = writeEnv;
|
||||
fields.WRITTEN_COUNT = written;
|
||||
}
|
||||
if (primary.note) {
|
||||
fields.NOTE = primary.note;
|
||||
}
|
||||
|
||||
// Additional credentials (e.g. Slack has botToken + appToken)
|
||||
if (results.length > 1) {
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
const extra = results[i];
|
||||
const suffix = `_${i + 1}`;
|
||||
fields[`HAS_CREDENTIAL${suffix}`] = !!extra.resolved;
|
||||
fields[`CREDENTIAL_SOURCE${suffix}`] = extra.source;
|
||||
fields[`CREDENTIAL_MASKED${suffix}`] = extra.masked || 'none';
|
||||
fields[`NANOCLAW_ENV_VAR${suffix}`] = extra.envVar;
|
||||
if (extra.note) {
|
||||
fields[`NOTE${suffix}`] = extra.note;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emitStatus(fields);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,484 @@
|
||||
---
|
||||
name: migrate-nanoclaw
|
||||
description: Extracts user customizations from a fork, generates a replayable migration guide, and upgrades to upstream by reapplying customizations on a clean base. Replaces merge-based upgrades with intent-based migration.
|
||||
---
|
||||
|
||||
# Context
|
||||
|
||||
NanoClaw users fork the repo and customize it — changing config values, editing source files, modifying personas, adding skills. When upstream ships updates or refactors, `git merge` produces painful conflicts because the same core files were changed on both sides.
|
||||
|
||||
This skill extracts the user's customizations into a migration guide — capturing both the intent (what they want) and the implementation details (how they did it, with code snippets, API calls, and specific configurations). On upgrade, it checks out clean upstream in a worktree, then reapplies customizations using the guide. No merge conflicts because there's nothing to merge.
|
||||
|
||||
The migration guide is markdown, not structured data. It needs to capture the full range of what a user might customize, with enough implementation detail that a fresh Claude session can reapply it without having seen the original code. Standard changes (config values, simple logic) can be described briefly. Non-standard changes (specific APIs, custom integrations, unusual patterns) need code snippets and precise instructions.
|
||||
|
||||
Two phases: **Extract** (build the migration guide) and **Upgrade** (use it). If a guide already exists, offer to skip to Upgrade.
|
||||
|
||||
# Principles
|
||||
|
||||
- Never proceed with a dirty working tree.
|
||||
- Always create a rollback point (backup branch + tag) before touching anything.
|
||||
- The migration guide is the source of truth, not diffs.
|
||||
- Use a worktree to validate before affecting the live install.
|
||||
- Data directories (`groups/`, `store/`, `data/`, `.env`) are never touched — only code.
|
||||
- Be helpful: offer to do things (stash, commit, stop services) rather than telling the user to do them.
|
||||
- **Use sub-agents for exploration.** Spawn haiku sub-agents to explore the codebase, trace skill merges, diff files, and identify customizations. This keeps the main context focused on the user conversation and decision-making.
|
||||
- **Always use absolute paths in worktrees.** The Bash tool resets the working directory between calls. Never use relative `cd .upgrade-worktree` — always use the full absolute path: `cd /absolute/path/.upgrade-worktree && <command>`. Store the worktree absolute path in a variable at creation time and reference it throughout.
|
||||
- **Balance exploration and asking.** Don't bombard the user with questions when you can figure things out from the code. Don't burn endless tokens exploring when the user could clarify in one sentence. Use sub-agents to explore first, then ask the user targeted questions about things that are ambiguous or where intent isn't clear from the code alone.
|
||||
- **Scale effort to complexity.** Not every migration needs the full process. Assess the scope early and take the lightest path that fits.
|
||||
|
||||
---
|
||||
|
||||
# Phase 1: Extract
|
||||
|
||||
## 1.0 Preflight
|
||||
|
||||
Run `git status --porcelain`. If non-empty, offer to stash or commit for them (AskUserQuestion: "Stash changes" / "Commit changes" / "I'll handle it"). If they want to commit, stage and commit with a descriptive message. If they want to stash, run `git stash push -m "pre-migration stash"`.
|
||||
|
||||
Check remotes with `git remote -v`. If `upstream` is missing, ask for the URL (default: `https://github.com/qwibitai/nanoclaw.git`), add it, then `git fetch upstream --prune`.
|
||||
|
||||
Detect upstream branch: check `git branch -r | grep upstream/` for `main` or `master`. Store as UPSTREAM_BRANCH.
|
||||
|
||||
## 1.1 Assess scope and determine path
|
||||
|
||||
Quickly assess the scale of divergence, check for an existing guide, and determine the right approach — all before asking the user anything.
|
||||
|
||||
```bash
|
||||
BASE=$(git merge-base HEAD upstream/$UPSTREAM_BRANCH)
|
||||
# Divergence stats
|
||||
git rev-list --count $BASE..upstream/$UPSTREAM_BRANCH # upstream commits
|
||||
git rev-list --count $BASE..HEAD # user commits
|
||||
git diff --name-only $BASE..HEAD | wc -l # user changed files
|
||||
git diff --stat $BASE..HEAD | tail -1 # insertions/deletions
|
||||
git diff --name-only $BASE..upstream/$UPSTREAM_BRANCH | wc -l # upstream changed files
|
||||
```
|
||||
|
||||
Check for existing guide: `.nanoclaw-migrations/guide.md` or `.nanoclaw-migrations/index.md`.
|
||||
|
||||
**Determine the tier based on the total diff from base:**
|
||||
|
||||
### Tier 1: Lightweight — suggest `/update-nanoclaw` instead
|
||||
|
||||
Conditions (any of):
|
||||
- Very few upstream changes (< ~5 commits) AND few user changes (< ~3 changed files)
|
||||
- User recently updated/migrated (merge-base is close to upstream HEAD)
|
||||
|
||||
Tell the user the scope is small and suggest `/update-nanoclaw` might be simpler. Let them choose.
|
||||
|
||||
### Tier 2: Standard
|
||||
|
||||
Conditions:
|
||||
- Moderate total diff (3-15 changed files, no large number of new files)
|
||||
- Manageable scope that fits in a single guide file
|
||||
|
||||
### Tier 3: Complex
|
||||
|
||||
Conditions (any of):
|
||||
- Many new files added (indicates many skills applied) — discount files that come purely from skill merges when assessing complexity; a fork with 3 skills and no other changes is simpler than it looks by file count alone
|
||||
- Deep source changes to core files (`src/index.ts`, `src/container-runner.ts`, etc.) beyond what skills introduced
|
||||
- Lots of insertions/deletions in user-authored code (not skill-merged code)
|
||||
- Many skills applied (3+) AND the user confirms or sub-agents find customizations on top of them
|
||||
|
||||
Use the full process: multiple sub-agents in parallel, directory-based guide, migration plan.
|
||||
|
||||
**Now combine the scope assessment with initial user input in one interaction.** Present the scope summary (how many commits, files, which tier) and ask (AskUserQuestion):
|
||||
|
||||
For Tier 1:
|
||||
- **Use /update-nanoclaw** — simpler merge-based approach
|
||||
- **Proceed with full migration** — continue
|
||||
|
||||
For Tier 2/3 (with or without existing guide):
|
||||
- If guide exists and is current: **Skip to upgrade** / **Update guide** (add new changes) / **Re-extract from scratch**
|
||||
- If guide exists but is stale: **Update guide** (recommended) / **Re-extract from scratch** / **Skip to upgrade anyway**
|
||||
- If no guide: **Yes, let me describe my customizations first** / **Just figure it out** / **A bit of both**
|
||||
|
||||
This single interaction replaces what were previously separate steps for scope assessment, user input, and existing guide check.
|
||||
|
||||
## 1.2 Update existing guide (if applicable)
|
||||
|
||||
If the user chose to update an existing guide rather than re-extract:
|
||||
|
||||
1. Read the existing guide
|
||||
2. Find commits made since the guide was generated (compare guide's recorded base hash against current HEAD)
|
||||
3. Spawn a haiku sub-agent to analyze only the new changes:
|
||||
> Diff HEAD against `<guide-recorded-hash>`. For each changed file, summarize what changed and why.
|
||||
4. Present the new changes to the user for confirmation
|
||||
5. Append new customizations to the existing guide, update the header hashes
|
||||
6. Skip to Phase 2
|
||||
|
||||
## 1.3 Explore the codebase
|
||||
|
||||
Spawn a haiku sub-agent (Agent tool, model: haiku) for initial exploration:
|
||||
|
||||
> Explore this NanoClaw fork to identify all changes from the upstream base. Run these commands and report back:
|
||||
>
|
||||
> 1. `git diff --name-only $BASE..HEAD` — all changed files
|
||||
> 2. `git log --oneline $BASE..HEAD` — all commits (look for skill branch merges like `Merge branch 'skill/*'`)
|
||||
> 3. `git branch -r --list 'upstream/skill/*'` — available upstream skill branches
|
||||
> 4. `ls .claude/skills/` — installed skills
|
||||
> 5. For each skill merge found, record the merge commit hash
|
||||
>
|
||||
> Report: (a) list of applied skills with their merge commit hashes, (b) list of all changed files, (c) any custom skill directories that don't match upstream branches.
|
||||
|
||||
From the sub-agent results, identify:
|
||||
- **Which files came purely from skill merges** — these will be reapplied by re-merging skill branches in Phase 2
|
||||
- **Everything else** — all remaining changes are customizations to analyze (whether they're on skill-touched files or not)
|
||||
|
||||
Don't try to distinguish "user modified a skill file" from "user made their own change" at this stage. The sub-agents in 1.4 will look at all non-skill changes together and surface what matters.
|
||||
|
||||
## 1.4 Analyze customizations
|
||||
|
||||
For each applied skill, ask the user in a single batched question (AskUserQuestion, multiSelect):
|
||||
|
||||
> "I found these applied skills. Select any you customized further after applying:"
|
||||
|
||||
Options: one per skill, plus "None — all used as-is".
|
||||
|
||||
Then spawn sub-agents to analyze all non-skill changes. For Tier 2, one or two agents. For Tier 3, run in parallel by area:
|
||||
|
||||
- **Config + build files** — one sub-agent
|
||||
- **Source files** (`src/*.ts`) — one sub-agent
|
||||
- **Skills the user flagged as modified** (or all of them for Tier 3) — one sub-agent per skill, comparing the user's current files against the skill merge commit version:
|
||||
```
|
||||
git diff <merge-commit-hash>..HEAD -- <files-touched-by-skill>
|
||||
```
|
||||
- **Container files** — one sub-agent (if changes exist)
|
||||
|
||||
Each sub-agent task:
|
||||
|
||||
> Read these diffs and the current file contents. For each change:
|
||||
> 1. `git diff $BASE..HEAD -- <file>` (or `git diff <skill-merge-hash>..HEAD -- <file>` for skill-modified files)
|
||||
> 2. Read the full current file for context
|
||||
> 3. Summarize: what changed, what the likely intent is
|
||||
> 4. Assess detail level: could a fresh Claude session reproduce this from intent alone, or does it need specific code snippets, API details, import paths?
|
||||
> 5. For non-standard changes, extract the key code, imports, API calls, and configurations verbatim.
|
||||
|
||||
**Inter-skill conflicts:** If multiple skills are applied, spawn an additional sub-agent to check for interactions between them. Look for:
|
||||
- Duplicate declarations (same variable/constant defined by two skill branches)
|
||||
- Conflicting approaches (one skill throws on missing env var, another provides a fallback)
|
||||
- Shared files modified by multiple skills
|
||||
|
||||
Document any findings in the "Skill Interactions" section of the migration guide so they can be resolved after skill branches are re-merged during upgrade.
|
||||
|
||||
## 1.5 Confirm with user
|
||||
|
||||
After sub-agents report back, compile the findings and present to the user.
|
||||
|
||||
For customizations where the intent is clear (config values, simple modifications): present as a batch for confirmation. Use AskUserQuestion with multiSelect to let the user flag any entries that need correction.
|
||||
|
||||
For customizations where the intent is ambiguous: ask specific questions. Don't ask "what did you do?" — instead ask "I see you added X in this file. Was this for Y or something else?"
|
||||
|
||||
The user can select "Other" on any question to provide their own description.
|
||||
|
||||
## 1.6 Migration plan (Tier 3 only)
|
||||
|
||||
For complex migrations, before writing the guide, create a migration plan:
|
||||
|
||||
- **Order of operations**: which customizations depend on others, which skills must be applied first
|
||||
- **Staging**: whether the migration should happen in stages (e.g. apply skills first, validate, then apply source customizations)
|
||||
- **Risk areas**: customizations that touch files heavily changed by upstream — these may need manual review
|
||||
- **Interactions**: customizations that interact with each other (e.g. a source change that depends on a skill, or two customizations that touch the same file)
|
||||
|
||||
Present the plan to the user for review before proceeding to the guide.
|
||||
|
||||
## 1.7 Write the migration guide
|
||||
|
||||
**Storage:** `.nanoclaw-migrations/guide.md` for Tier 2. `.nanoclaw-migrations/` directory with `index.md` and section files for Tier 3.
|
||||
|
||||
**Verification:** After writing the guide, read it back and verify:
|
||||
- Every referenced file path exists in the current codebase
|
||||
- Code snippets match what's actually in the files
|
||||
- No customizations from the analysis were accidentally omitted
|
||||
|
||||
The guide is structured markdown that a fresh Claude session can follow to reproduce this user's exact setup on a clean upstream checkout.
|
||||
|
||||
Structure:
|
||||
|
||||
```markdown
|
||||
# NanoClaw Migration Guide
|
||||
|
||||
Generated: <timestamp>
|
||||
Base: <BASE hash>
|
||||
HEAD at generation: <HEAD hash>
|
||||
Upstream: <upstream HEAD hash>
|
||||
|
||||
## Migration Plan
|
||||
|
||||
(Tier 3 only — big-picture overview of order, staging, risks)
|
||||
|
||||
## Applied Skills
|
||||
|
||||
List each skill with its branch name. These are reapplied by merging the upstream skill branch.
|
||||
|
||||
- `add-telegram` — branch `skill/telegram`
|
||||
- `add-voice-transcription` — branch `skill/voice-transcription`
|
||||
|
||||
Custom skills (user-created, not from upstream): `.claude/skills/my-custom-skill/` — copy as-is from main tree.
|
||||
|
||||
## Skill Interactions
|
||||
|
||||
(Document known conflicts or interactions between applied skills.
|
||||
When two or more skills modify the same file or depend on shared
|
||||
config, describe the conflict and how to resolve it after merging.
|
||||
Example: skill A and skill B both add a PROXY_BIND_HOST declaration —
|
||||
after merging both, deduplicate. Or: skill A throws if ENV_VAR is
|
||||
missing, but skill B provides a fallback — use the fallback version.)
|
||||
|
||||
## Modifications to Applied Skills
|
||||
|
||||
### <Skill name>: <what was modified>
|
||||
|
||||
**Intent:** ...
|
||||
|
||||
**Files:** ...
|
||||
|
||||
**How to apply:** (after the skill branch has been merged)
|
||||
|
||||
...
|
||||
|
||||
## Customizations
|
||||
|
||||
### <Descriptive title for customization>
|
||||
|
||||
**Intent:** What the user wants and why.
|
||||
|
||||
**Files:** Which files to modify.
|
||||
|
||||
**How to apply:**
|
||||
|
||||
<For standard changes, a brief description is enough.>
|
||||
|
||||
<For non-standard changes, include code snippets, API details,
|
||||
specific values, import paths — everything needed to reproduce
|
||||
without seeing the original diff.>
|
||||
|
||||
### <Next customization...>
|
||||
```
|
||||
|
||||
**Judging detail level:** For each customization, assess whether a fresh Claude session could reproduce it from intent alone:
|
||||
- **Standard changes** (config values, simple logic, well-known patterns): describe the intent and the target. Example: "Change `POLL_INTERVAL` in `src/config.ts` from 2000 to 1000."
|
||||
- **Non-standard changes** (specific API usage, custom integrations, unusual patterns, library-specific configurations): include the actual code snippets, import paths, API endpoints, configuration objects — everything needed to reproduce it without guessing.
|
||||
|
||||
Example entries at different detail levels:
|
||||
|
||||
**Standard (brief):**
|
||||
```markdown
|
||||
### Custom trigger word
|
||||
|
||||
**Intent:** Use `@Bob` instead of the default `@Andy`.
|
||||
|
||||
**Files:** `src/config.ts`
|
||||
|
||||
**How to apply:** Change the default value of `ASSISTANT_NAME` from `'Andy'` to `'Bob'`.
|
||||
```
|
||||
|
||||
**Non-standard (detailed):**
|
||||
```markdown
|
||||
### Spanish translation for outbound messages
|
||||
|
||||
**Intent:** All outbound messages are translated to Spanish before sending. Uses the DeepL API via the `deepl-node` package.
|
||||
|
||||
**Files:** `src/router.ts`, `package.json`
|
||||
|
||||
**How to apply:**
|
||||
|
||||
1. Add dependency: `npm install deepl-node`
|
||||
|
||||
2. In `src/router.ts`, add import at top:
|
||||
```typescript
|
||||
import * as deepl from 'deepl-node';
|
||||
const translator = new deepl.Translator(process.env.DEEPL_API_KEY!);
|
||||
```
|
||||
|
||||
3. In the `formatOutbound` function, before the return statement, add:
|
||||
```typescript
|
||||
const result = await translator.translateText(text, null, 'es');
|
||||
text = result.text;
|
||||
```
|
||||
Note: the function needs to be made async if it isn't already.
|
||||
```
|
||||
|
||||
After writing, offer to commit for the user:
|
||||
```bash
|
||||
git add .nanoclaw-migrations/
|
||||
git commit -m "chore: save migration guide"
|
||||
```
|
||||
|
||||
Ask (AskUserQuestion): "Migration guide saved. Want to upgrade now or later?"
|
||||
- **Upgrade now** — continue to Phase 2
|
||||
- **Later** — stop here
|
||||
|
||||
---
|
||||
|
||||
# Phase 2: Upgrade
|
||||
|
||||
## 2.0 Preflight
|
||||
|
||||
Same checks as 1.0 — clean tree (offer to stash/commit if dirty), upstream configured, fetch latest.
|
||||
|
||||
Read the migration guide. If missing, tell the user you need to extract customizations first and ask if they want to do that now.
|
||||
|
||||
**New-changes guard:** Compare the guide's "HEAD at generation" hash against current HEAD. If there are commits since the guide was generated, warn the user:
|
||||
|
||||
> "You've made changes since the migration guide was generated. These changes won't be included in the upgrade."
|
||||
|
||||
AskUserQuestion:
|
||||
- **Update the guide first** — go to step 1.2 to incorporate new changes
|
||||
- **Proceed anyway** — user accepts that recent changes will be lost
|
||||
- **Abort** — stop
|
||||
|
||||
## 2.1 Safety net
|
||||
|
||||
```bash
|
||||
HASH=$(git rev-parse --short HEAD)
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
git branch backup/pre-migrate-$HASH-$TIMESTAMP
|
||||
git tag pre-migrate-$HASH-$TIMESTAMP
|
||||
```
|
||||
|
||||
Save the tag name for rollback instructions at the end.
|
||||
|
||||
## 2.2 Preview upstream changes
|
||||
|
||||
```bash
|
||||
BASE=$(git merge-base HEAD upstream/$UPSTREAM_BRANCH)
|
||||
git log --oneline $BASE..upstream/$UPSTREAM_BRANCH
|
||||
git diff $BASE..upstream/$UPSTREAM_BRANCH -- CHANGELOG.md
|
||||
```
|
||||
|
||||
If there are `[BREAKING]` entries, show them and explain how they interact with the user's customizations from the migration guide.
|
||||
|
||||
Ask (AskUserQuestion) to proceed or abort.
|
||||
|
||||
## 2.3 Create upgrade worktree
|
||||
|
||||
```bash
|
||||
PROJECT_ROOT=$(pwd)
|
||||
git worktree add .upgrade-worktree upstream/$UPSTREAM_BRANCH --detach
|
||||
WORKTREE="$PROJECT_ROOT/.upgrade-worktree"
|
||||
```
|
||||
|
||||
Store `$PROJECT_ROOT` and `$WORKTREE` as absolute paths. Use `$WORKTREE` in all subsequent commands — never `cd .upgrade-worktree` with a relative path.
|
||||
|
||||
## 2.4 Reapply skills in worktree
|
||||
|
||||
For each skill listed in the migration guide's "Applied Skills" section:
|
||||
|
||||
1. Check if branch exists: `git branch -r --list "upstream/$branch"`
|
||||
2. If yes, merge it in the worktree:
|
||||
```bash
|
||||
cd "$WORKTREE" && git merge upstream/skill/<name> --no-edit
|
||||
```
|
||||
3. If missing, warn the user (skill may have been removed or renamed upstream).
|
||||
4. If any skill merge conflicts, stop and tell the user — the skill needs updating for the new upstream.
|
||||
|
||||
Copy any custom skills mentioned in the guide from the main tree into the worktree.
|
||||
|
||||
## 2.5 Reapply customizations in worktree
|
||||
|
||||
Work in `.upgrade-worktree/`. Follow each customization section in the migration guide, including "Modifications to Applied Skills."
|
||||
|
||||
For Tier 3 migrations with a migration plan, follow the plan's ordering and staging. If the plan calls for staged validation (e.g. validate after skills, then validate after source changes), do so.
|
||||
|
||||
For each customization:
|
||||
1. Read the "How to apply" instructions from the guide
|
||||
2. Read the target file(s) in the worktree to understand the current upstream version
|
||||
3. Apply the changes as described — use the code snippets and specific instructions from the guide
|
||||
4. If the target file has changed significantly from what the guide expects (function removed, file restructured, API changed), flag it and ask the user what to do
|
||||
5. Verify the file has no syntax errors or broken imports after each change
|
||||
|
||||
For behavior customizations (CLAUDE.md files): copy from the main tree. These are user content, not code.
|
||||
|
||||
## 2.6 Validate in worktree
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
## 2.7 Live test (optional)
|
||||
|
||||
Ask (AskUserQuestion):
|
||||
- **Test live** — stop service, run from worktree against real data, send a test message
|
||||
- **Skip** — trust the build, proceed to swap
|
||||
|
||||
If testing live:
|
||||
|
||||
1. Stop the service (do this directly):
|
||||
```bash
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist 2>/dev/null || true
|
||||
```
|
||||
|
||||
2. Symlink data into the worktree:
|
||||
```bash
|
||||
ln -s "$PROJECT_ROOT/store" "$WORKTREE/store"
|
||||
ln -s "$PROJECT_ROOT/data" "$WORKTREE/data"
|
||||
ln -s "$PROJECT_ROOT/groups" "$WORKTREE/groups"
|
||||
ln -s "$PROJECT_ROOT/.env" "$WORKTREE/.env"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
5. After confirmation, stop the dev server.
|
||||
|
||||
6. Clean up symlinks:
|
||||
```bash
|
||||
rm "$WORKTREE/store" "$WORKTREE/data" "$WORKTREE/groups" "$WORKTREE/.env"
|
||||
```
|
||||
|
||||
## 2.8 Swap into main tree
|
||||
|
||||
The swap must be done carefully — the worktree has the upgraded code, but main needs to point to it cleanly. Use absolute paths throughout.
|
||||
|
||||
```bash
|
||||
# 1. Capture the worktree HEAD before removing it
|
||||
WORKTREE_PATH=$(cd "$PROJECT_ROOT/.upgrade-worktree" && pwd)
|
||||
UPGRADE_COMMIT=$(git -C "$WORKTREE_PATH" rev-parse HEAD)
|
||||
|
||||
# 2. Copy the migration guide out of the worktree before removing it
|
||||
cp -r "$WORKTREE_PATH/.nanoclaw-migrations" /tmp/nanoclaw-migrations-backup 2>/dev/null || true
|
||||
|
||||
# 3. Remove the worktree
|
||||
git worktree remove "$WORKTREE_PATH" --force
|
||||
|
||||
# 4. Point the current branch at the upgraded commit
|
||||
git reset --hard $UPGRADE_COMMIT
|
||||
|
||||
# 5. Restore the migration guide and update its hashes
|
||||
cp -r /tmp/nanoclaw-migrations-backup/* .nanoclaw-migrations/ 2>/dev/null || true
|
||||
rm -rf /tmp/nanoclaw-migrations-backup
|
||||
```
|
||||
|
||||
Update the guide's header hashes to reflect the new state. Offer to commit:
|
||||
```bash
|
||||
git add .nanoclaw-migrations/
|
||||
git commit -m "chore: upgrade to upstream $(git rev-parse --short upstream/$UPSTREAM_BRANCH)"
|
||||
```
|
||||
|
||||
Do NOT use `git checkout -B` to create an intermediate branch — this caused issues in practice. The `git reset --hard` to the upgrade commit is the cleanest path since the backup tag already preserves the pre-upgrade state.
|
||||
|
||||
## 2.9 Post-upgrade
|
||||
|
||||
Run `npm install && pnpm run build` in the main tree to confirm.
|
||||
|
||||
Restart the service:
|
||||
```bash
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
```
|
||||
|
||||
Show summary:
|
||||
- Previous version (backup tag)
|
||||
- New HEAD
|
||||
- Customizations reapplied (list from guide)
|
||||
- Skills reapplied
|
||||
- Rollback: `git reset --hard <backup-tag>`
|
||||
- Any customizations that needed manual adjustment
|
||||
|
||||
Offer to pop the stash if one was created in preflight: `git stash pop`
|
||||
|
||||
## Diagnostics
|
||||
|
||||
1. Use the Read tool to read `.claude/skills/migrate-nanoclaw/diagnostics.md`.
|
||||
2. Follow every step in that file before finishing.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Diagnostics
|
||||
|
||||
Gather system info:
|
||||
|
||||
```bash
|
||||
node -p "require('./package.json').version"
|
||||
uname -s
|
||||
uname -m
|
||||
node -p "process.versions.node.split('.')[0]"
|
||||
```
|
||||
|
||||
Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP addresses.
|
||||
|
||||
```json
|
||||
{
|
||||
"api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP",
|
||||
"event": "migrate_complete",
|
||||
"distinct_id": "<uuid>",
|
||||
"properties": {
|
||||
"success": true,
|
||||
"nanoclaw_version": "1.2.43",
|
||||
"os_platform": "darwin",
|
||||
"arch": "arm64",
|
||||
"node_major_version": 22,
|
||||
"migration_phase": "extract|upgrade|both",
|
||||
"tier": 2,
|
||||
"customization_count": 3,
|
||||
"skills_applied_count": 2,
|
||||
"skill_interaction_count": 0,
|
||||
"live_test": false,
|
||||
"breaking_changes_found": false,
|
||||
"error_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Show the entire JSON to the user and ask via AskUserQuestion: **Yes** / **No** / **Never ask again**
|
||||
|
||||
**Yes**:
|
||||
```bash
|
||||
curl -s -X POST https://us.i.posthog.com/capture/ -H 'Content-Type: application/json' -d @/tmp/nanoclaw-diagnostics.json
|
||||
rm /tmp/nanoclaw-diagnostics.json
|
||||
```
|
||||
|
||||
**No**: `rm /tmp/nanoclaw-diagnostics.json`
|
||||
|
||||
**Never ask again**:
|
||||
1. Replace contents of `.claude/skills/setup/diagnostics.md` with `# Diagnostics — opted out`
|
||||
2. Replace contents of `.claude/skills/update-nanoclaw/diagnostics.md` with `# Diagnostics — opted out`
|
||||
3. Replace contents of `.claude/skills/migrate-nanoclaw/diagnostics.md` with `# Diagnostics — opted out`
|
||||
4. Remove the diagnostics sections from each corresponding SKILL.md
|
||||
5. `rm /tmp/nanoclaw-diagnostics.json`
|
||||
+154
-108
@@ -1,58 +1,50 @@
|
||||
---
|
||||
name: setup
|
||||
description: Run initial NanoClaw setup. Use when user wants to install dependencies, authenticate messaging channels, register their main channel, or start the background services. Triggers on "setup", "install", "configure nanoclaw", or first-time setup requests.
|
||||
allowed-tools: Bash(git remote*), Bash(bash setup.sh), Bash(pnpm exec tsx setup/index.ts*)
|
||||
---
|
||||
|
||||
# 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
|
||||
---
|
||||
|
||||
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
|
||||
**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.
|
||||
|
||||
```!
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git 2>/dev/null || true
|
||||
git remote -v
|
||||
```
|
||||
Verify with `git remote -v`.
|
||||
|
||||
If continue without fork: add upstream so they can still pull updates:
|
||||
```bash
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git
|
||||
```
|
||||
|
||||
**Case B — `origin` points to user's fork, no `upstream` remote:**
|
||||
|
||||
Add upstream:
|
||||
```bash
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git
|
||||
```
|
||||
|
||||
**Case C — both `origin` (user's fork) and `upstream` (qwibitai) exist:**
|
||||
|
||||
Already configured. Continue.
|
||||
|
||||
**Verify:** `git remote -v` should show `origin` → user's repo, `upstream` → `qwibitai/nanoclaw.git`.
|
||||
|
||||
## 1. Bootstrap (Node.js + Dependencies)
|
||||
|
||||
Run `bash setup.sh` and parse the status block.
|
||||
Output from !`bash setup.sh` — parse the status block.
|
||||
|
||||
- If NODE_OK=false → Node.js is missing or too old. Use `AskUserQuestion: Would you like me to install Node.js 22?` If confirmed:
|
||||
- macOS: `brew install node@22` (if brew available) or install nvm then `nvm install 22`
|
||||
@@ -64,69 +56,69 @@ 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.
|
||||
Output from !`pnpm exec tsx setup/index.ts --step environment` — 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
|
||||
|
||||
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."
|
||||
3. **Migrate later** — "Continue setup now, run `/migrate-from-openclaw` anytime later."
|
||||
|
||||
If "Migrate now": invoke `/migrate-from-openclaw`, then return here and continue at step 2a (Timezone).
|
||||
|
||||
## 2a. Timezone
|
||||
|
||||
Run `npx tsx setup/index.ts --step timezone` and parse the status block.
|
||||
Output from !`pnpm exec tsx setup/index.ts --step timezone` — 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:
|
||||
|
||||
@@ -146,14 +138,14 @@ grep -q '.local/bin' ~/.zshrc 2>/dev/null || echo 'export PATH="$HOME/.local/bin
|
||||
|
||||
Then re-verify with `onecli version`.
|
||||
|
||||
Point the CLI at the local OneCLI instance (it defaults to the cloud service otherwise):
|
||||
Point the CLI at the local OneCLI instance, the ONECLI_URL was output from the install script above:
|
||||
```bash
|
||||
onecli config set api-host http://127.0.0.1:10254
|
||||
onecli config set api-host ${ONECLI_URL}
|
||||
```
|
||||
|
||||
Ensure `.env` has the OneCLI URL (create the file if it doesn't exist):
|
||||
```bash
|
||||
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=http://127.0.0.1:10254' >> .env
|
||||
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=${ONECLI_URL}' >> .env
|
||||
```
|
||||
|
||||
Check if a secret already exists:
|
||||
@@ -178,7 +170,7 @@ Then stop and wait for the user to confirm they have the token. Do NOT proceed u
|
||||
|
||||
Once they confirm, they register it with OneCLI. AskUserQuestion with two options:
|
||||
|
||||
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
|
||||
1. **Dashboard** — description: "Best if you have a browser on this machine. Open ${ONECLI_URL} and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
|
||||
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_TOKEN --host-pattern api.anthropic.com`"
|
||||
|
||||
#### API key path
|
||||
@@ -187,7 +179,7 @@ Tell the user to get an API key from https://console.anthropic.com/settings/keys
|
||||
|
||||
Then AskUserQuestion with two options:
|
||||
|
||||
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI."
|
||||
1. **Dashboard** — description: "Best if you have a browser on this machine. Open ${ONECLI_URL} and add the secret in the UI."
|
||||
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_KEY --host-pattern api.anthropic.com`"
|
||||
|
||||
#### After either path
|
||||
@@ -198,48 +190,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. Invoke `/use-native-credential-proxy` to set up the built-in credential proxy instead. That skill handles credential collection, `.env` configuration, and verification.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -247,7 +256,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.
|
||||
|
||||
@@ -271,27 +280,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 http://127.0.0.1:10254/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.
|
||||
|
||||
@@ -302,3 +331,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.
|
||||
|
||||
@@ -9,6 +9,8 @@ uname -m
|
||||
node -p "process.versions.node.split('.')[0]"
|
||||
```
|
||||
|
||||
Check if the user migrated from OpenClaw during this setup session (i.e. `/migrate-from-openclaw` was invoked). If you're unsure (e.g. after context compaction), check for `migration-state.md` in the project root — it exists during and sometimes after migration.
|
||||
|
||||
Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP addresses.
|
||||
|
||||
```json
|
||||
@@ -23,6 +25,7 @@ Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP ad
|
||||
"arch": "arm64",
|
||||
"node_major_version": 22,
|
||||
"channels_selected": ["telegram", "whatsapp"],
|
||||
"migrated_from_openclaw": false,
|
||||
"error_count": 0,
|
||||
"failed_step": null
|
||||
}
|
||||
|
||||
@@ -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,9 +109,11 @@ 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.
|
||||
|
||||
Present these buckets to the user and ask them to choose one path using AskUserQuestion:
|
||||
- A) **Full update**: merge all upstream changes
|
||||
- B) **Selective update**: cherry-pick specific upstream commits
|
||||
@@ -173,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.
|
||||
@@ -188,7 +190,7 @@ After validation succeeds, check if the update introduced any breaking changes.
|
||||
Determine which CHANGELOG entries are new by diffing against the backup tag:
|
||||
- `git diff <backup-tag-from-step-1>..HEAD -- CHANGELOG.md`
|
||||
|
||||
Parse the diff output for lines starting with `+[BREAKING]`. Each such line is one breaking change entry. The format is:
|
||||
Parse the diff output for lines that contain `[BREAKING]` anywhere in the line. Each such line is one breaking change entry. The format is:
|
||||
```
|
||||
[BREAKING] <description>. Run `/<skill-name>` to <action>.
|
||||
```
|
||||
@@ -232,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.
|
||||
|
||||
@@ -76,8 +76,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```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 checkout --theirs pnpm-lock.yaml
|
||||
git add pnpm-lock.yaml
|
||||
git merge --continue
|
||||
}
|
||||
```
|
||||
@@ -87,7 +87,7 @@ This modifies `src/transcription.ts` to use the `whisper-cli` binary instead of
|
||||
### Validate
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Phase 3: Verify
|
||||
@@ -110,7 +110,7 @@ launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
### Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.npm-cache/
|
||||
# pnpm content-addressable store (created when running in sandbox mode)
|
||||
.pnpm-store/
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
npm run format:fix
|
||||
pnpm run format:fix
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# 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
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
|
||||
+5
-1
@@ -4,9 +4,13 @@ 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).
|
||||
|
||||
## [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`.
|
||||
|
||||
## [1.2.35] - 2026-03-26
|
||||
|
||||
- [BREAKING] OneCLI Agent Vault replaces the built-in credential proxy. Existing `.env` credentials must be migrated to the vault. Run `/init-onecli` to install OneCLI and migrate credentials.
|
||||
- [BREAKING] OneCLI Agent Vault replaces the built-in credential proxy. Check your runtime: `grep CONTAINER_RUNTIME_BIN src/container-runtime.ts` — if it shows `'container'` you are on Apple Container, if `'docker'` you are on Docker. Docker users: run `/init-onecli` to install OneCLI and migrate `.env` credentials to the vault. Apple Container users: re-merge the skill branch (`git fetch upstream skill/apple-container && git merge upstream/skill/apple-container`) then run `/convert-to-apple-container` and follow all instructions (configures credential proxy networking) — do NOT run `/init-onecli`, it requires Docker.
|
||||
|
||||
## [1.2.21] - 2026-03-22
|
||||
|
||||
|
||||
@@ -1,80 +1,196 @@
|
||||
# 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/access.ts` | `pickApprover`, `pickApprovalDelivery`, admin resolution for `NANOCLAW_ADMIN_USER_IDS` |
|
||||
| `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` / `request_rebuild`** — changes to the per-agent-group container config only (apt/npm deps, wire an existing MCP server). Admin approval, rebuild, container restart. `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`.
|
||||
|
||||
## 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/checklist.md](docs/checklist.md) | Rolling status checklist across all subsystems |
|
||||
| [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.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
community@nanoclaw.dev.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -16,3 +16,11 @@ Thanks to everyone who has contributed to NanoClaw!
|
||||
- [flobo3](https://github.com/flobo3) — Flo
|
||||
- [edwinwzhe](https://github.com/edwinwzhe) — Edwin He
|
||||
- [scottgl9](https://github.com/scottgl9) — Scott Glover
|
||||
- [cschmidt](https://github.com/cschmidt) — Carl Schmidt
|
||||
- [leonalfredbot-ship-it](https://github.com/leonalfredbot-ship-it) — Alfred-the-buttler
|
||||
- [moktamd](https://github.com/moktamd)
|
||||
- [gurixs-carson](https://github.com/gurixs-carson)
|
||||
- [MrBlaise](https://github.com/MrBlaise) — Balázs Rostás
|
||||
- [lbsnrs](https://github.com/lbsnrs) — Andreas Liebschner
|
||||
- [spencer-whitman](https://github.com/spencer-whitman)
|
||||
- [lazure-ocean](https://github.com/lazure-ocean) — Cyril Ionov
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
# NanoClaw Refactor — Forward-Looking Reference
|
||||
|
||||
Consolidates what's still relevant from `REFACTOR_PLAN.md` and `REFACTOR_EXECUTION.md`: open decisions, remaining work, operational patterns worth keeping. Historical PR timeline and phase framing have been dropped — the work is in the commit history.
|
||||
|
||||
---
|
||||
|
||||
## Architecture (still authoritative)
|
||||
|
||||
### Module tiers
|
||||
|
||||
Three categories, distinguished by shipping model and dependency direction:
|
||||
|
||||
| Tier | Where it lives | Loaded by default? | Removal cost |
|
||||
|------|----------------|--------------------|--------------|
|
||||
| **Core** | `src/**` (outside `src/modules/`, `src/channels/`, `src/providers/`) | always | N/A — can't remove |
|
||||
| **Default modules** | `src/modules/<name>/` on main | yes — imported by `src/modules/index.ts` | edit core imports (intentional friction) |
|
||||
| **Optional modules** | `src/modules/<name>/` on main (for now — see open q #7) | yes, via barrel import | delete files + barrel line + revert `MODULE-HOOK` edits |
|
||||
| **Channel adapters** | `src/channels/<name>.ts` on `channels` branch | no — cherry-pick via `/add-<name>` | delete files + barrel line |
|
||||
| **Providers** | on `providers` branch | no — cherry-pick via `/add-<provider>` | delete files + barrel line |
|
||||
|
||||
Default modules today: `typing`, `mount-security`, `approvals`, `cli`.
|
||||
Optional modules: `interactive`, `scheduling`, `permissions`, `agent-to-agent`, `self-mod`.
|
||||
|
||||
Dependency rule: **core ← default modules ← optional modules**. Optional modules must not depend on each other. Known transitional violation (flagged): `src/db/messaging-groups.ts` auto-wires `agent_destinations` when agent-to-agent is installed.
|
||||
|
||||
### The four registries
|
||||
|
||||
Full contract in [`docs/module-contract.md`](docs/module-contract.md). Summary:
|
||||
|
||||
1. **Delivery action handlers** — `delivery.ts`; modules call `registerDeliveryAction(name, fn)`.
|
||||
2. **Router inbound gate** — `router.ts`; single setter (`setSenderResolver` + `setAccessGate`). Default: allow-all.
|
||||
3. **Response dispatcher** — `response-registry.ts`; modules call `registerResponseHandler(fn)`. First to return `true` claims.
|
||||
4. **Container MCP tool self-registration** — `container/agent-runner/src/mcp-tools/server.ts`; modules call `registerTools([...])` at import.
|
||||
|
||||
Anything else single-consumer uses either a `sqlite_master`-guarded inline read or a `MODULE-HOOK:<name>:start/end` skill edit.
|
||||
|
||||
### Module distribution (pending)
|
||||
|
||||
- **`main`** — core + default modules + default channel (`cli`). Ships clean.
|
||||
- **`channels`** — fully loaded runnable branch with all channel adapters; skills cherry-pick from it.
|
||||
- **`providers`** — same pattern for agent providers (OpenCode).
|
||||
- **`modules` branch** — proposed but NOT created yet. See "Remaining work" below.
|
||||
|
||||
---
|
||||
|
||||
## Remaining work
|
||||
|
||||
### Phase 5: merge `v2` → `main`
|
||||
|
||||
Cut-over the refactor. Pre-reqs (already met): green build, green tests, green service boot, clean `channels` / `providers` syncs.
|
||||
|
||||
Open logistics:
|
||||
- Release versioning: bump to `1.3.0` at merge time or cut a `v2-rc` tag first for internal testing? Non-blocking — decide at merge.
|
||||
- Coordinate with anyone still running the old `main` (v1.2.53) — breaking change for them.
|
||||
- Announce the new layout + the one shell command that changed (`pnpm run chat` is new default).
|
||||
|
||||
### `modules` branch — create, skip, or defer?
|
||||
|
||||
The original plan (PR #10) was to fork a `modules` branch and populate it with the 5 optional modules, so future `/add-<module>` skills pull via `git show origin/modules:path`. Three paths:
|
||||
|
||||
- **(a) Create it now.** Matches the `channels`/`providers` pattern for consistency. Extra surface to maintain: every core change must be merged into `modules` at phase boundaries (same cadence as channels/providers). Pays off if we ever want to make a module *truly* optional (not shipped on main).
|
||||
- **(b) Skip it.** Leave all 5 optional modules shipped on main. No `modules` branch, no install skills, no cherry-picking. Simpler but loses the "opt-in" property for users who want a leaner install.
|
||||
- **(c) Defer.** Ship main without the modules branch; create it later if someone actually wants to slim their install. No-cost option for now.
|
||||
|
||||
Recommendation leans toward (c) — we've already paid the architectural cost (tier boundary, dependency rule, registries) without needing the branch today.
|
||||
|
||||
### Per-module follow-ups (tracked as open questions below)
|
||||
|
||||
Each has a specific landing zone when we get to it:
|
||||
- #11–13 (admin mechanism, providers registry, container-runner audit) — scope a focused cleanup pass.
|
||||
- #14 (CLAUDE.md review) — single dedicated PR touching every module.
|
||||
- #15 (A2A / destinations rethink) — requires design, not just cleanup.
|
||||
- #17–18 (self-mod rethink, per-group source) — requires design.
|
||||
- #19 (system vs user CLAUDE.md) — requires install-skill tooling.
|
||||
|
||||
---
|
||||
|
||||
## Operational patterns (keep using these)
|
||||
|
||||
### Standing checks for every PR
|
||||
|
||||
Non-negotiable; a unit test suite alone doesn't catch circular-import TDZ bugs:
|
||||
|
||||
1. `pnpm run build` clean.
|
||||
2. `pnpm test` + `bun test` (in `container/agent-runner/`) all green.
|
||||
3. **Service actually starts.** `gtimeout 5 node dist/index.js` (or `launchctl kickstart`) must reach `NanoClaw running`. Unit tests import individual files; only `main()` exercises the module-init order.
|
||||
4. Expected boot log lines present (at least: `Central DB ready`, `Delivery polls started`, `Host sweep started`, `NanoClaw running`, plus any module lifecycle line like `OneCLI approval handler started` or `CLI channel listening`).
|
||||
|
||||
### Module architecture rule (TDZ bug, PR #3)
|
||||
|
||||
Any registry state a module writes to at import time must live in a file with **no back-edge to `src/index.ts`** — transitively. `src/index.ts` imports `src/modules/index.js` for side effects; if a module calls `registerX()` at top level and `X` lives in `src/index.ts`, the ES module loader hits a TDZ reference on the const declaration. Fix: registry state lives in its own dependency-free file (e.g. `src/response-registry.ts`). Any new registry follows the same pattern.
|
||||
|
||||
### Branch sync procedure
|
||||
|
||||
After every `v2` (or future `main`) sync into `channels` / `providers` / future `modules`:
|
||||
|
||||
1. **File-presence diff.** Enumerate files that existed pre-sync but are missing post-sync:
|
||||
```
|
||||
git ls-tree -r <pre-sync> | awk '{print $4}' | sort > /tmp/pre.txt
|
||||
git ls-tree -r <post-sync> | awk '{print $4}' | sort > /tmp/post.txt
|
||||
comm -23 /tmp/pre.txt /tmp/post.txt
|
||||
```
|
||||
Classify each missing file:
|
||||
- **Intentional** (core deleted it) → leave deleted.
|
||||
- **Branch-owned** (channels branch still needs it) → restore from pre-sync HEAD.
|
||||
|
||||
This has caught real losses on both `channels` (17 adapter files plus 3 setup scripts after PR #2's channel move) and `providers` (opencode files after PR #2).
|
||||
|
||||
2. **Cross-file consistency.** When restoring a file, check whether something *else* that also changed references it (e.g. `setup/index.ts`'s `STEPS` map).
|
||||
|
||||
3. **Run the standing checks** against the synced branch (not just v2).
|
||||
|
||||
### Prettier drift pattern
|
||||
|
||||
The `format:fix` pre-commit hook sometimes reformats peer files *after* the commit completes, leaving cosmetic-only diffs in the working tree. Discard with `git checkout -- <files>`. Do not re-commit the drift — it's trivial whitespace and noise.
|
||||
|
||||
---
|
||||
|
||||
## Open questions (curated)
|
||||
|
||||
### Design / architecture
|
||||
|
||||
1. **`NANOCLAW_ADMIN_USER_IDS` as the admin mechanism.** Host queries `user_roles` at container wake, collapses into env var, container compares sender IDs. Conflates identity-at-send with privilege-at-wake and forces the container to care about namespaced user IDs. Revisit during a container-runner audit.
|
||||
|
||||
2. **Host-side `src/providers/` registry.** One real consumer (OpenCode). A registry is probably overkill — the install skill could just edit `container-runner.ts` via `MODULE-HOOK`. Fold into the container-runner audit.
|
||||
|
||||
3. **Container-runner audit.** `src/container-runner.ts` has accreted wake/spawn/kill, mount assembly, OneCLI credential application, admin-ID env var, idle timers, image rebuild. Some pieces should pull apart or move into modules. Not blocking. Related to #1 and #2.
|
||||
|
||||
4. **Revisit destinations + A2A capability holistically.** The destination projection invariant, dual-purpose routing+ACL table, channel vs agent destination shapes, `createMessagingGroupAgent` auto-wire coupling — more machinery than the feature warrants. Phase 3 moved it out of core intact; a redesign is warranted but scoped post-refactor.
|
||||
|
||||
5. **Self-mod approach rethink.** Three separate MCP tools + three delivery actions + three approval handlers for what's essentially "mutate container.config.json and rebuild." Also: post-rebuild latency (host sweep waits up to 60s), and agents sometimes send redundant `add_mcp_server` + `request_rebuild` pairs. Consider collapsing into a single "apply this container-config diff" approval primitive.
|
||||
|
||||
6. **Per-agent-group source / per-group base image.** Self-mod today layers packages/MCP on a shared base. As groups diverge (different base images, provider configs, runtime toolchains), the shared-base assumption won't scale. Scope post-refactor.
|
||||
|
||||
### Distribution / operational
|
||||
|
||||
7. **Providers on a consolidated `modules` branch?** Staying separate for now. Revisit if a second optional provider appears.
|
||||
|
||||
8. **Per-group module enablement.** Modules are currently project-wide. If one agent group wants approvals and another doesn't, we'd need per-group feature flags. Flag if asked.
|
||||
|
||||
9. **Module removal UX.** We do not drop tables on uninstall. Is that the right default? (Alternative: `/remove-<module>` optionally runs a down migration. YAGNI until requested.)
|
||||
|
||||
10. **Cross-module ordering for the response dispatcher.** Registration order determines who claims a given `questionId`. IDs are disjoint in practice (`q-…` vs `appr-…`), so first-match-wins is safe. If a third response-consuming module arrives, we may need keyed dispatch.
|
||||
|
||||
11. **Versioned module migrations.** Reinstalls are idempotent (migrator skips anything already in `schema_version`). If a module ships a *new* migration in a later version, the install skill must append the new file + barrel entry without touching prior ones. Simplest rule: install skills are additive; content changes to an already-applied migration are a hard error.
|
||||
|
||||
12. **Telegram pairing imports from permissions (channels branch).** `src/channels/telegram.ts` reaches into `src/modules/permissions/db/*` for `grantRole`/`hasAnyOwner`/`upsertUser` in the pairing-bootstrap branch. Cross-branch tier violation. Fix: extract those writes into a pairing helper (e.g. `src/channels/telegram-pairing-accept.ts` or `setup/pair-telegram.ts`). Non-blocking.
|
||||
|
||||
### Core slotting (files not explicitly discussed)
|
||||
|
||||
13. **`state-sqlite.ts`, `webhook-server.ts`, `timezone.ts`.** state-sqlite is likely core (host tracker). Webhook-server likely core (channel infra). Timezone likely core utility. Confirm if any of them prove to be module-shaped during future audits.
|
||||
|
||||
14. **Chat SDK bridge location.** `src/channels/chat-sdk-bridge.ts` is channel infra that bridges adapters on the `channels` branch. Stays in `src/channels/` for now.
|
||||
|
||||
15. **OneCLI credential injection.** Lives in `container-runner.ts`. Every agent call uses it, no clean optional boundary. Stays core. Related: `onecli-approvals.ts` is bundled inside the `approvals` default module on the assumption OneCLI stays in core. If OneCLI later moves to its own module, `onecli-approvals` follows.
|
||||
|
||||
### Documentation
|
||||
|
||||
16. **CLAUDE.md content per module.** Every module ships with project.md + agent.md. Need a dedicated review pass: (a) write the missing agent-to-agent snippets, (b) audit other modules for accuracy/tone, (c) confirm `agent.md` files are actually tailored for the agent vs. copy-pastes of `project.md`.
|
||||
|
||||
17. **Split system CLAUDE.md from user CLAUDE.md.** Project `CLAUDE.md` and `groups/global/CLAUDE.md` mix system-authored content (module contracts, install-skill appends) with user customizations. Updates currently risk clobbering user intent. Look at a system-owned region (or separate file) that skills rewrite freely plus a user-owned one that's never touched. Related to #16.
|
||||
|
||||
---
|
||||
|
||||
## Where the canonical references live
|
||||
|
||||
- **Module contract** — [`docs/module-contract.md`](docs/module-contract.md)
|
||||
- **Architecture overview** — [`docs/architecture.md`](docs/architecture.md)
|
||||
- **DB layout** — [`docs/db.md`](docs/db.md), [`docs/db-central.md`](docs/db-central.md), [`docs/db-session.md`](docs/db-session.md)
|
||||
- **Agent-runner internals** — [`docs/agent-runner-details.md`](docs/agent-runner-details.md)
|
||||
- **Channel isolation model** — [`docs/isolation-model.md`](docs/isolation-model.md)
|
||||
- **Build + runtime split** — [`docs/build-and-runtime.md`](docs/build-and-runtime.md)
|
||||
- **Top-level** — [`CLAUDE.md`](CLAUDE.md)
|
||||
|
||||
This doc (`REFACTOR.md`) is transient — prune when open questions close; retire entirely once the refactor is fully behind us and the operational patterns have been absorbed into `CLAUDE.md` or `docs/`.
|
||||
@@ -0,0 +1,2 @@
|
||||
agent-runner/node_modules
|
||||
agent-runner/dist
|
||||
+92
-49
@@ -1,70 +1,113 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# NanoClaw Agent Container
|
||||
# Runs Claude Agent SDK in isolated Linux VM with browser automation
|
||||
# Runs Claude Agent SDK in isolated Linux VM with browser automation.
|
||||
#
|
||||
# Runtime split:
|
||||
# - agent-runner (our TypeScript code): Bun
|
||||
# - globally-installed Node CLIs (claude-code, agent-browser, vercel): pnpm + Node
|
||||
|
||||
FROM node:22-slim
|
||||
|
||||
# Install system dependencies for Chromium
|
||||
RUN apt-get update && apt-get install -y \
|
||||
chromium \
|
||||
fonts-liberation \
|
||||
fonts-noto-cjk \
|
||||
fonts-noto-color-emoji \
|
||||
libgbm1 \
|
||||
libnss3 \
|
||||
libatk-bridge2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libx11-xcb1 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxrandr2 \
|
||||
libasound2 \
|
||||
libpangocairo-1.0-0 \
|
||||
libcups2 \
|
||||
libdrm2 \
|
||||
libxshmfence1 \
|
||||
curl \
|
||||
git \
|
||||
# ---- Build-time arguments ----------------------------------------------------
|
||||
# CJK fonts add ~200MB. Opt in only if you render Chinese/Japanese/Korean text.
|
||||
ARG INSTALL_CJK_FONTS=false
|
||||
|
||||
# Pin CLI versions for reproducibility. Bump deliberately — unpinned installs
|
||||
# mean every rebuild silently picks up the latest and can break in lockstep
|
||||
# across all users.
|
||||
ARG CLAUDE_CODE_VERSION=2.1.112
|
||||
ARG AGENT_BROWSER_VERSION=latest
|
||||
ARG VERCEL_VERSION=latest
|
||||
ARG BUN_VERSION=1.3.12
|
||||
|
||||
# ---- System dependencies -----------------------------------------------------
|
||||
# tini: correct PID 1 / signal forwarding so outbound.db writes finalize on
|
||||
# SIGTERM instead of being orphaned by the shell entrypoint.
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
chromium \
|
||||
fonts-liberation \
|
||||
fonts-noto-color-emoji \
|
||||
libgbm1 \
|
||||
libnss3 \
|
||||
libatk-bridge2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libx11-xcb1 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxrandr2 \
|
||||
libasound2 \
|
||||
libpangocairo-1.0-0 \
|
||||
libcups2 \
|
||||
libdrm2 \
|
||||
libxshmfence1 \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
tini \
|
||||
unzip \
|
||||
&& if [ "$INSTALL_CJK_FONTS" = "true" ]; then \
|
||||
apt-get install -y --no-install-recommends fonts-noto-cjk; \
|
||||
fi \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set Chromium path for agent-browser
|
||||
# Chromium path for agent-browser / Playwright consumers
|
||||
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
# Belt-and-braces: prevent Playwright's postinstall from downloading its own
|
||||
# ~300MB Chromium. We've already installed the system one above.
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
# Install agent-browser and claude-code globally
|
||||
RUN npm install -g agent-browser @anthropic-ai/claude-code
|
||||
# ---- Bun runtime -------------------------------------------------------------
|
||||
# Install via the official script (handles multi-arch detection), then move
|
||||
# the binary to /usr/local/bin so the non-root `node` user can execute it.
|
||||
RUN curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" && \
|
||||
install -m 0755 /root/.bun/bin/bun /usr/local/bin/bun && \
|
||||
rm -rf /root/.bun
|
||||
|
||||
# Create app directory
|
||||
# ---- pnpm + global Node CLIs -------------------------------------------------
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
# agent-browser has a postinstall build script — pnpm skips these by default.
|
||||
# Allowlist it via .npmrc so the install doesn't silently produce a broken
|
||||
# package. Pinned versions so every rebuild is reproducible.
|
||||
RUN --mount=type=cache,target=/root/.cache/pnpm \
|
||||
echo "only-built-dependencies[]=agent-browser" > /root/.npmrc && \
|
||||
pnpm install -g \
|
||||
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
|
||||
"agent-browser@${AGENT_BROWSER_VERSION}" \
|
||||
"vercel@${VERCEL_VERSION}"
|
||||
|
||||
# ---- agent-runner ------------------------------------------------------------
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files first for better caching
|
||||
COPY agent-runner/package*.json ./
|
||||
# Copy manifest + lockfile first so the install layer caches independently of
|
||||
# source edits.
|
||||
COPY agent-runner/package.json agent-runner/bun.lock ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||
bun install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
# Source. Bun runs TS directly — no tsc build step. The host remounts this
|
||||
# path at runtime via `src/container-runner.ts` so source edits on the host
|
||||
# take effect without rebuilding the image; the baked copy is the fallback.
|
||||
COPY agent-runner/ ./
|
||||
|
||||
# Build TypeScript
|
||||
RUN npm run build
|
||||
# ---- Entrypoint --------------------------------------------------------------
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
# Create workspace directories
|
||||
RUN mkdir -p /workspace/group /workspace/global /workspace/extra /workspace/ipc/messages /workspace/ipc/tasks /workspace/ipc/input
|
||||
# ---- Workspace + permissions -------------------------------------------------
|
||||
RUN mkdir -p /workspace/group /workspace/global /workspace/extra && \
|
||||
chown -R node:node /workspace && \
|
||||
chmod 755 /home/node
|
||||
|
||||
# Create entrypoint script
|
||||
# Container input (prompt, group info) is passed via stdin JSON.
|
||||
# Credentials are injected by the host's credential proxy — never passed here.
|
||||
# Follow-up messages arrive via IPC files in /workspace/ipc/input/
|
||||
RUN printf '#!/bin/bash\nset -e\ncd /app && npx tsc --outDir /tmp/dist 2>&1 >&2\nln -s /app/node_modules /tmp/dist/node_modules\nchmod -R a-w /tmp/dist\ncat > /tmp/input.json\nnode /tmp/dist/index.js < /tmp/input.json\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh
|
||||
|
||||
# Set ownership to node user (non-root) for writable directories
|
||||
RUN chown -R node:node /workspace && chmod 777 /home/node
|
||||
|
||||
# Switch to non-root user (required for --dangerously-skip-permissions)
|
||||
USER node
|
||||
|
||||
# Set working directory to group workspace
|
||||
WORKDIR /workspace/group
|
||||
|
||||
# Entry point reads JSON from stdin, outputs JSON to stdout
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
# tini is PID 1, reaps zombies, forwards signals cleanly. entrypoint.sh does
|
||||
# `exec bun ...` so bun runs as tini's direct child.
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/app/entrypoint.sh"]
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "nanoclaw-agent-runner",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"cron-parser": "^5.0.0",
|
||||
"zod": "^4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"typescript": "^5.7.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.112", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-vMFoiDKlOive8p3tphpV1gQaaytOipwGJ+uw9mvvaLQUODSC2+fCdRDAY25i2Tsv+lOtxzXBKctmaDuWqZY7ig=="],
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
||||
|
||||
"@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cron-parser": ["cron-parser@5.5.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
}
|
||||
}
|
||||
Generated
-1524
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user