botschat 0.1.10 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +11 -15
  2. package/migrations/0012_push_tokens.sql +11 -0
  3. package/package.json +20 -1
  4. package/packages/api/src/do/connection-do.ts +142 -24
  5. package/packages/api/src/env.ts +6 -0
  6. package/packages/api/src/index.ts +7 -0
  7. package/packages/api/src/routes/auth.ts +85 -9
  8. package/packages/api/src/routes/channels.ts +3 -2
  9. package/packages/api/src/routes/dev-auth.ts +45 -0
  10. package/packages/api/src/routes/push.ts +52 -0
  11. package/packages/api/src/routes/upload.ts +73 -38
  12. package/packages/api/src/utils/fcm.ts +167 -0
  13. package/packages/api/src/utils/firebase.ts +218 -0
  14. package/packages/plugin/dist/src/channel.d.ts +6 -0
  15. package/packages/plugin/dist/src/channel.d.ts.map +1 -1
  16. package/packages/plugin/dist/src/channel.js +71 -15
  17. package/packages/plugin/dist/src/channel.js.map +1 -1
  18. package/packages/plugin/package.json +1 -1
  19. package/packages/web/dist/assets/index-B9qN5gs6.js +1 -0
  20. package/packages/web/dist/assets/index-BQNMGVyU.js +2 -0
  21. package/packages/web/dist/assets/{index-Ev5M8VmV.css → index-Bd_RDcgO.css} +1 -1
  22. package/packages/web/dist/assets/index-Civeg2lm.js +1 -0
  23. package/packages/web/dist/assets/index-Dk33VSnY.js +2 -0
  24. package/packages/web/dist/assets/index-Kr85Nj_-.js +1516 -0
  25. package/packages/web/dist/assets/index-lVB82JKU.js +1 -0
  26. package/packages/web/dist/assets/index.esm-CtMkqqqb.js +599 -0
  27. package/packages/web/dist/assets/web-CUXjh_UA.js +1 -0
  28. package/packages/web/dist/assets/web-vKLTVUul.js +1 -0
  29. package/packages/web/dist/index.html +6 -4
  30. package/packages/web/dist/sw.js +158 -1
  31. package/packages/web/index.html +4 -2
  32. package/packages/web/package.json +4 -1
  33. package/packages/web/src/App.tsx +117 -1
  34. package/packages/web/src/api.ts +21 -1
  35. package/packages/web/src/components/AccountSettings.tsx +131 -0
  36. package/packages/web/src/components/ChatWindow.tsx +302 -70
  37. package/packages/web/src/components/CronSidebar.tsx +89 -24
  38. package/packages/web/src/components/DataConsentModal.tsx +249 -0
  39. package/packages/web/src/components/LoginPage.tsx +55 -7
  40. package/packages/web/src/components/MessageContent.tsx +71 -9
  41. package/packages/web/src/components/MobileLayout.tsx +28 -118
  42. package/packages/web/src/components/SessionTabs.tsx +41 -2
  43. package/packages/web/src/components/Sidebar.tsx +88 -66
  44. package/packages/web/src/e2e.ts +26 -5
  45. package/packages/web/src/firebase.ts +215 -3
  46. package/packages/web/src/foreground.ts +51 -0
  47. package/packages/web/src/index.css +10 -2
  48. package/packages/web/src/main.tsx +24 -2
  49. package/packages/web/src/push.ts +205 -0
  50. package/packages/web/src/ws.ts +20 -8
  51. package/scripts/dev.sh +158 -26
  52. package/scripts/mock-openclaw.mjs +382 -0
  53. package/scripts/test-e2e-chat.ts +2 -2
  54. package/scripts/test-e2e-live.ts +1 -1
  55. package/wrangler.toml +3 -0
  56. package/packages/web/dist/assets/index-DpW6VzZK.js +0 -1497
package/scripts/dev.sh CHANGED
@@ -1,24 +1,44 @@
1
1
  #!/usr/bin/env bash
2
2
  # BotsChat local dev startup script
3
3
  # Usage:
4
- # ./scripts/dev.sh — build web + migrate + start server
5
- # ./scripts/dev.sh reset — nuke local DB, re-migrate, then start
4
+ # ./scripts/dev.sh — full dev env: build + migrate + server + mock AI + open browser
5
+ # ./scripts/dev.sh reset — nuke local DB, re-migrate, then start full dev env
6
+ # ./scripts/dev.sh server — only start wrangler dev server (no mock, no browser)
6
7
  # ./scripts/dev.sh migrate — only run D1 migrations (no server)
7
8
  # ./scripts/dev.sh build — only build web frontend (no server)
8
9
  # ./scripts/dev.sh sync — sync plugin to mini.local + rebuild + restart gateway
9
10
  # ./scripts/dev.sh logs — tail gateway logs on mini.local
11
+ # ./scripts/dev.sh mock — start mock OpenClaw standalone (foreground)
10
12
  set -euo pipefail
11
13
 
12
14
  cd "$(dirname "$0")/.."
13
15
  ROOT="$(pwd)"
14
16
 
17
+ # ── Auto-set DEV_AUTH_SECRET ──────────────────────────────────────────
18
+ # For local dev, any string works. Use a fixed default so new developers
19
+ # can run `./scripts/dev.sh` without setting env vars first.
20
+ if [[ -z "${DEV_AUTH_SECRET:-}" ]]; then
21
+ DEV_AUTH_SECRET="botschat-local-dev-secret"
22
+ export DEV_AUTH_SECRET
23
+ fi
24
+
15
25
  # ── Colours ──────────────────────────────────────────────────────────
16
- RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; NC='\033[0m'
26
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; DIM='\033[2m'; NC='\033[0m'
17
27
  info() { echo -e "${CYAN}▸${NC} $*"; }
18
28
  ok() { echo -e "${GREEN}✔${NC} $*"; }
19
29
  warn() { echo -e "${YELLOW}▲${NC} $*"; }
20
30
  fail() { echo -e "${RED}✖${NC} $*"; exit 1; }
21
31
 
32
+ # ── Process tracking ─────────────────────────────────────────────────
33
+ WRANGLER_PID=""
34
+ MOCK_PID=""
35
+
36
+ cleanup() {
37
+ [[ -n "$MOCK_PID" ]] && kill "$MOCK_PID" 2>/dev/null || true
38
+ [[ -n "$WRANGLER_PID" ]] && kill "$WRANGLER_PID" 2>/dev/null || true
39
+ wait 2>/dev/null || true
40
+ }
41
+
22
42
  # ── Helpers ──────────────────────────────────────────────────────────
23
43
 
24
44
  kill_port() {
@@ -32,6 +52,55 @@ kill_port() {
32
52
  fi
33
53
  }
34
54
 
55
+ wait_for_server() {
56
+ info "Waiting for server…"
57
+ local max=60 i=0
58
+ while ! curl -sf --max-time 1 -o /dev/null http://localhost:8787/ 2>/dev/null; do
59
+ sleep 1
60
+ i=$((i + 1))
61
+ if [[ $i -ge $max ]]; then
62
+ fail "Server didn't start within ${max}s"
63
+ fi
64
+ done
65
+ }
66
+
67
+ get_mock_token() {
68
+ local BASE_URL="http://localhost:8787"
69
+ local TOKEN_JSON AUTH_TOKEN PAT_JSON PAT
70
+
71
+ TOKEN_JSON=$(curl -sf -X POST "$BASE_URL/api/dev-auth/login" \
72
+ -H 'Content-Type: application/json' \
73
+ -d "{\"secret\":\"$DEV_AUTH_SECRET\",\"userId\":\"dev-test-user\"}" 2>&1) || {
74
+ fail "Cannot reach $BASE_URL — is the server running?"
75
+ }
76
+ AUTH_TOKEN=$(echo "$TOKEN_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null) || {
77
+ fail "Failed to parse auth token: $TOKEN_JSON"
78
+ }
79
+
80
+ PAT_JSON=$(curl -sf -X POST "$BASE_URL/api/pairing-tokens" \
81
+ -H "Authorization: Bearer $AUTH_TOKEN" \
82
+ -H 'Content-Type: application/json' \
83
+ -d '{"label":"mock-openclaw"}' 2>&1) || {
84
+ fail "Failed to create pairing token"
85
+ }
86
+ PAT=$(echo "$PAT_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null) || {
87
+ fail "Failed to parse pairing token: $PAT_JSON"
88
+ }
89
+
90
+ echo "$PAT"
91
+ }
92
+
93
+ open_browser() {
94
+ local url="http://localhost:8787/?dev_token=${DEV_AUTH_SECRET}"
95
+ if command -v open &>/dev/null; then
96
+ open "$url"
97
+ elif command -v xdg-open &>/dev/null; then
98
+ xdg-open "$url"
99
+ else
100
+ warn "Open in browser: $url"
101
+ fi
102
+ }
103
+
35
104
  do_migrate() {
36
105
  info "Applying D1 migrations (local)…"
37
106
  npx wrangler d1 migrations apply botschat-db --local
@@ -51,44 +120,98 @@ do_build_web() {
51
120
  ok "Web build complete (packages/web/dist)"
52
121
  }
53
122
 
123
+ # ── Server-only start (foreground, no mock/browser) ──────────────────
124
+
54
125
  do_start() {
55
126
  kill_port 8787
56
127
  info "Starting wrangler dev on 0.0.0.0:8787…"
57
- exec npx wrangler dev --config wrangler.toml --ip 0.0.0.0 --var ENVIRONMENT:development
128
+ exec npx wrangler dev --config wrangler.toml --ip 0.0.0.0 --var ENVIRONMENT:development --var DEV_AUTH_SECRET:"$DEV_AUTH_SECRET"
129
+ }
130
+
131
+ # ── Full dev environment (server + mock + browser) ───────────────────
132
+
133
+ do_start_full() {
134
+ kill_port 8787
135
+ trap cleanup EXIT INT TERM
136
+
137
+ info "Starting wrangler dev on 0.0.0.0:8787…"
138
+ npx wrangler dev --config wrangler.toml --ip 0.0.0.0 \
139
+ --var ENVIRONMENT:development \
140
+ --var DEV_AUTH_SECRET:"$DEV_AUTH_SECRET" &
141
+ WRANGLER_PID=$!
142
+
143
+ wait_for_server
144
+ ok "Server ready on http://localhost:8787"
145
+
146
+ info "Starting Mock OpenClaw…"
147
+ local PAT
148
+ PAT=$(get_mock_token)
149
+ mkdir -p "$ROOT/.wrangler"
150
+ node "$ROOT/scripts/mock-openclaw.mjs" --token "$PAT" > "$ROOT/.wrangler/mock-openclaw.log" 2>&1 &
151
+ MOCK_PID=$!
152
+ ok "Mock OpenClaw connected (pid=$MOCK_PID)"
153
+
154
+ open_browser
155
+
156
+ echo ""
157
+ echo -e "${CYAN}╭──────────────────────────────────────────────────╮${NC}"
158
+ echo -e "${CYAN}│${NC} ${GREEN}BotsChat Dev Environment Ready${NC} ${CYAN}│${NC}"
159
+ echo -e "${CYAN}│${NC} ${CYAN}│${NC}"
160
+ echo -e "${CYAN}│${NC} Server: http://localhost:8787 ${CYAN}│${NC}"
161
+ echo -e "${CYAN}│${NC} Mock AI: running ${DIM}(log: .wrangler/mock-openclaw.log)${NC}"
162
+ echo -e "${CYAN}│${NC} Auth: auto-login enabled ${CYAN}│${NC}"
163
+ echo -e "${CYAN}│${NC} ${CYAN}│${NC}"
164
+ echo -e "${CYAN}│${NC} Press ${YELLOW}Ctrl+C${NC} to stop all services ${CYAN}│${NC}"
165
+ echo -e "${CYAN}╰──────────────────────────────────────────────────╯${NC}"
166
+ echo ""
167
+
168
+ wait $WRANGLER_PID 2>/dev/null || true
169
+ }
170
+
171
+ # ── Standalone mock (foreground) ─────────────────────────────────────
172
+
173
+ do_mock() {
174
+ info "Getting auth token via dev-auth…"
175
+ local PAT
176
+ PAT=$(get_mock_token)
177
+ ok "Pairing token: ${PAT:0:16}***"
178
+ info "Starting Mock OpenClaw…"
179
+ exec node "$ROOT/scripts/mock-openclaw.mjs" --token "$PAT" "$@"
58
180
  }
59
181
 
182
+ # ── Sync plugin to mini.local ────────────────────────────────────────
183
+
60
184
  do_sync_plugin() {
61
- local REMOTE_USER="mini.local"
62
- local REMOTE_DIR="~/Projects/botsChat/packages/plugin"
185
+ local REMOTE="mini.local"
186
+ local DEV_DIR="~/Projects/botschat-app/botsChat/packages/plugin"
187
+ local EXT_DIR="~/.openclaw/extensions/botschat"
63
188
 
64
- info "Syncing plugin to mini.local…"
189
+ info "Syncing plugin source to mini.local dev repo…"
65
190
  rsync -avz --exclude node_modules --exclude .git --exclude dist --exclude .wrangler \
66
- packages/plugin/ "$REMOTE_USER:$REMOTE_DIR/"
67
- ok "Plugin files synced"
191
+ packages/plugin/ "$REMOTE:$DEV_DIR/"
192
+ ok "Plugin source synced → $DEV_DIR"
68
193
 
69
- info "Building plugin, deploying to extensions, restarting gateway on mini.local…"
70
- ssh "$REMOTE_USER" 'export PATH="/opt/homebrew/bin:$PATH"
71
- cd ~/Projects/botsChat/packages/plugin
194
+ info "Building plugin in dev repo, deploying to extensions, restarting gateway…"
195
+ ssh "$REMOTE" "export PATH=\"/opt/homebrew/bin:\$PATH\"
196
+ cd $DEV_DIR
72
197
  npm run build
73
- EXT_DIR=~/.openclaw/extensions/botschat
74
- rsync -av --delete dist/ "$EXT_DIR/dist/"
75
- rsync -av bin/ "$EXT_DIR/bin/" 2>/dev/null || true
76
- cp -f package.json openclaw.plugin.json "$EXT_DIR/" 2>/dev/null || true
77
- echo "--- Deployed to $EXT_DIR ---"
78
- pkill -9 -f openclaw-gateway 2>/dev/null || true
79
- sleep 3
80
- nohup openclaw gateway run --bind loopback --port 18789 --force > /tmp/openclaw-gateway.log 2>&1 &
81
- echo "Gateway restarted (PID=$!)"'
82
- ok "Plugin synced, deployed to extensions, gateway restarted"
198
+ echo '--- Deploying built artifacts to $EXT_DIR ---'
199
+ rsync -av --delete dist/ $EXT_DIR/dist/
200
+ rsync -av bin/ $EXT_DIR/bin/ 2>/dev/null || true
201
+ cp -f package.json openclaw.plugin.json $EXT_DIR/ 2>/dev/null || true
202
+ echo '--- Restarting gateway via launchctl ---'
203
+ launchctl kickstart -k gui/\$(id -u)/ai.openclaw.gateway
204
+ echo 'Gateway restarted'"
205
+ ok "Plugin deployed to extensions, gateway restarted"
83
206
 
84
207
  sleep 4
85
208
  info "Checking connection…"
86
- ssh "$REMOTE_USER" 'tail -5 /tmp/openclaw-gateway.log | grep -i "authenticated\|error\|Task scan"'
209
+ ssh "$REMOTE" 'tail -10 ~/.openclaw/logs/gateway.log | grep -i "authenticated\|error\|Task scan\|botschat"'
87
210
  }
88
211
 
89
212
  do_logs() {
90
213
  info "Tailing gateway logs on mini.local…"
91
- ssh mini.local 'tail -f /tmp/openclaw-gateway.log'
214
+ ssh mini.local 'tail -f ~/.openclaw/logs/gateway.log'
92
215
  }
93
216
 
94
217
  # ── Main ─────────────────────────────────────────────────────────────
@@ -99,6 +222,11 @@ case "$cmd" in
99
222
  reset)
100
223
  do_reset
101
224
  do_build_web
225
+ do_start_full
226
+ ;;
227
+ server)
228
+ do_build_web
229
+ do_migrate
102
230
  do_start
103
231
  ;;
104
232
  migrate)
@@ -113,10 +241,14 @@ case "$cmd" in
113
241
  logs)
114
242
  do_logs
115
243
  ;;
244
+ mock)
245
+ shift
246
+ do_mock "$@"
247
+ ;;
116
248
  *)
117
- # Default: build + migrate + start
249
+ # Default: full dev experience — build + migrate + server + mock + browser
118
250
  do_build_web
119
251
  do_migrate
120
- do_start
252
+ do_start_full
121
253
  ;;
122
254
  esac
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mock OpenClaw — a lightweight WebSocket client that simulates an OpenClaw
4
+ * plugin for local BotsChat development. No OpenClaw dependency required.
5
+ *
6
+ * Usage:
7
+ * node scripts/mock-openclaw.mjs --token bc_pat_xxx
8
+ * node scripts/mock-openclaw.mjs --token bc_pat_xxx --delay 500 --stream
9
+ *
10
+ * Options:
11
+ * --token <pat> Pairing token (required)
12
+ * --url <url> Server URL (default: http://localhost:8787)
13
+ * --agents <list> Comma-separated agent IDs (default: main)
14
+ * --delay <ms> Reply delay in ms (default: 300)
15
+ * --stream Enable streaming replies (chunk by chunk)
16
+ * --model <name> Default model name (default: mock/echo-1.0)
17
+ */
18
+
19
+ import { randomUUID } from "node:crypto";
20
+ import { parseArgs } from "node:util";
21
+
22
+ // ── CLI args ─────────────────────────────────────────────────────────
23
+
24
+ const { values: args } = parseArgs({
25
+ options: {
26
+ token: { type: "string" },
27
+ url: { type: "string", default: "http://localhost:8787" },
28
+ agents: { type: "string", default: "main" },
29
+ delay: { type: "string", default: "300" },
30
+ stream: { type: "boolean", default: false },
31
+ model: { type: "string", default: "mock/echo-1.0" },
32
+ help: { type: "boolean", short: "h", default: false },
33
+ },
34
+ strict: true,
35
+ });
36
+
37
+ if (args.help || !args.token) {
38
+ console.log(`Mock OpenClaw — simulate an OpenClaw plugin for local testing
39
+
40
+ Usage:
41
+ node scripts/mock-openclaw.mjs --token <pairing-token> [options]
42
+
43
+ Options:
44
+ --token <pat> Pairing token (required)
45
+ --url <url> Server URL (default: http://localhost:8787)
46
+ --agents <list> Comma-separated agent IDs (default: main)
47
+ --delay <ms> Reply delay in ms (default: 300)
48
+ --stream Enable streaming replies
49
+ --model <name> Default model name (default: mock/echo-1.0)
50
+ -h, --help Show this help`);
51
+ process.exit(args.help ? 0 : 1);
52
+ }
53
+
54
+ const TOKEN = args.token;
55
+ const SERVER_URL = args.url;
56
+ const AGENTS = args.agents.split(",").map((s) => s.trim());
57
+ const DELAY_MS = parseInt(args.delay, 10);
58
+ const STREAMING = args.stream;
59
+ const MODEL = args.model;
60
+
61
+ // ── Colours ──────────────────────────────────────────────────────────
62
+
63
+ const c = {
64
+ reset: "\x1b[0m",
65
+ dim: "\x1b[2m",
66
+ cyan: "\x1b[36m",
67
+ green: "\x1b[32m",
68
+ yellow:"\x1b[33m",
69
+ red: "\x1b[31m",
70
+ magenta:"\x1b[35m",
71
+ };
72
+
73
+ function log(icon, msg) {
74
+ const ts = new Date().toISOString().slice(11, 23);
75
+ console.log(`${c.dim}${ts}${c.reset} ${icon} ${msg}`);
76
+ }
77
+ const logInfo = (msg) => log(`${c.cyan}▸${c.reset}`, msg);
78
+ const logOk = (msg) => log(`${c.green}✔${c.reset}`, msg);
79
+ const logWarn = (msg) => log(`${c.yellow}▲${c.reset}`, msg);
80
+ const logErr = (msg) => log(`${c.red}✖${c.reset}`, msg);
81
+ const logRecv = (msg) => log(`${c.magenta}◂${c.reset}`, msg);
82
+ const logSend = (msg) => log(`${c.cyan}▸${c.reset}`, msg);
83
+
84
+ // ── Mock models ──────────────────────────────────────────────────────
85
+
86
+ const MOCK_MODELS = [
87
+ { id: "mock/echo-1.0", name: "Echo 1.0", provider: "mock" },
88
+ { id: "mock/echo-streaming", name: "Echo Streaming", provider: "mock" },
89
+ { id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic" },
90
+ { id: "openai/gpt-4o", name: "GPT-4o", provider: "openai" },
91
+ ];
92
+
93
+ // ── WebSocket connection ─────────────────────────────────────────────
94
+
95
+ const MIN_BACKOFF = 1_000;
96
+ const MAX_BACKOFF = 30_000;
97
+ let backoff = MIN_BACKOFF;
98
+ let ws = null;
99
+ let pingTimer = null;
100
+ let intentionalClose = false;
101
+ let userId = null;
102
+
103
+ function buildWsUrl() {
104
+ let host = SERVER_URL.replace(/^https?:\/\//, "");
105
+ const isPlainHttp = SERVER_URL.startsWith("http://");
106
+ const scheme = isPlainHttp ? "ws" : "wss";
107
+ return `${scheme}://${host}/api/gateway/mock?token=${encodeURIComponent(TOKEN)}`;
108
+ }
109
+
110
+ function connect() {
111
+ const url = buildWsUrl();
112
+ logInfo(`Connecting to ${url.replace(/token=.*/, "token=***")}`);
113
+
114
+ ws = new WebSocket(url);
115
+
116
+ ws.addEventListener("open", () => {
117
+ logInfo("Connected, sending auth…");
118
+ send({ type: "auth", token: TOKEN, agents: AGENTS, model: MODEL });
119
+ });
120
+
121
+ ws.addEventListener("message", (event) => {
122
+ const data = typeof event.data === "string" ? event.data : event.data.toString();
123
+ let msg;
124
+ try {
125
+ msg = JSON.parse(data);
126
+ } catch {
127
+ logErr(`Bad JSON: ${data.slice(0, 100)}`);
128
+ return;
129
+ }
130
+ handleMessage(msg);
131
+ });
132
+
133
+ ws.addEventListener("close", (event) => {
134
+ logWarn(`Disconnected: code=${event.code} reason=${event.reason || "?"}`);
135
+ stopPing();
136
+ if (!intentionalClose) scheduleReconnect();
137
+ });
138
+
139
+ ws.addEventListener("error", (event) => {
140
+ logErr(`WebSocket error: ${event.message || "unknown"}`);
141
+ });
142
+ }
143
+
144
+ function send(msg) {
145
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
146
+ ws.send(JSON.stringify(msg));
147
+ }
148
+
149
+ function scheduleReconnect() {
150
+ logInfo(`Reconnecting in ${backoff}ms…`);
151
+ setTimeout(() => {
152
+ backoff = Math.min(backoff * 2, MAX_BACKOFF);
153
+ connect();
154
+ }, backoff);
155
+ }
156
+
157
+ function startPing() {
158
+ stopPing();
159
+ pingTimer = setInterval(() => {
160
+ send({ type: "status", connected: true, agents: AGENTS, model: MODEL });
161
+ }, 25_000);
162
+ }
163
+
164
+ function stopPing() {
165
+ if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
166
+ }
167
+
168
+ // ── Message handlers ─────────────────────────────────────────────────
169
+
170
+ function handleMessage(msg) {
171
+ switch (msg.type) {
172
+ case "auth.ok":
173
+ userId = msg.userId;
174
+ backoff = MIN_BACKOFF;
175
+ logOk(`Authenticated (userId=${userId})`);
176
+ startPing();
177
+ break;
178
+
179
+ case "auth.fail":
180
+ logErr(`Auth failed: ${msg.reason}`);
181
+ intentionalClose = true;
182
+ ws?.close(4001, "auth failed");
183
+ break;
184
+
185
+ case "ping":
186
+ send({ type: "pong" });
187
+ break;
188
+
189
+ case "user.message":
190
+ logRecv(`[user.message] sessionKey=${msg.sessionKey} text="${truncate(msg.text, 80)}"`);
191
+ handleUserMessage(msg);
192
+ break;
193
+
194
+ case "user.media":
195
+ logRecv(`[user.media] sessionKey=${msg.sessionKey} url=${msg.mediaUrl}`);
196
+ setTimeout(() => {
197
+ send({
198
+ type: "agent.text",
199
+ sessionKey: msg.sessionKey,
200
+ text: `📎 Received media: ${msg.mediaUrl}`,
201
+ messageId: randomUUID(),
202
+ });
203
+ logSend("[agent.text] media acknowledgement");
204
+ }, DELAY_MS);
205
+ break;
206
+
207
+ case "user.command":
208
+ logRecv(`[user.command] command=${msg.command} args=${msg.args || ""}`);
209
+ setTimeout(() => {
210
+ send({
211
+ type: "agent.text",
212
+ sessionKey: msg.sessionKey,
213
+ text: `Command received: /${msg.command} ${msg.args || ""}`.trim(),
214
+ messageId: randomUUID(),
215
+ });
216
+ }, DELAY_MS);
217
+ break;
218
+
219
+ case "user.action":
220
+ logRecv(`[user.action] action=${msg.action} params=${JSON.stringify(msg.params)}`);
221
+ setTimeout(() => {
222
+ send({
223
+ type: "agent.text",
224
+ sessionKey: msg.sessionKey,
225
+ text: `Action received: ${msg.action}`,
226
+ messageId: randomUUID(),
227
+ });
228
+ }, DELAY_MS);
229
+ break;
230
+
231
+ case "task.scan.request":
232
+ logRecv("[task.scan.request]");
233
+ send({ type: "task.scan.result", tasks: [] });
234
+ logSend("[task.scan.result] empty tasks");
235
+ break;
236
+
237
+ case "models.request":
238
+ logRecv("[models.request]");
239
+ send({ type: "models.list", models: MOCK_MODELS });
240
+ logSend(`[models.list] ${MOCK_MODELS.length} models`);
241
+ break;
242
+
243
+ case "task.schedule":
244
+ logRecv(`[task.schedule] cronJobId=${msg.cronJobId} schedule=${msg.schedule}`);
245
+ send({
246
+ type: "task.schedule.ack",
247
+ cronJobId: msg.cronJobId || `mock_cron_${Date.now()}`,
248
+ taskId: msg.taskId,
249
+ ok: true,
250
+ });
251
+ logSend("[task.schedule.ack] ok");
252
+ break;
253
+
254
+ case "task.delete":
255
+ logRecv(`[task.delete] cronJobId=${msg.cronJobId}`);
256
+ break;
257
+
258
+ case "task.run":
259
+ logRecv(`[task.run] cronJobId=${msg.cronJobId}`);
260
+ handleTaskRun(msg);
261
+ break;
262
+
263
+ case "settings.defaultModel":
264
+ logRecv(`[settings.defaultModel] model=${msg.defaultModel}`);
265
+ send({ type: "defaultModel.updated", model: msg.defaultModel });
266
+ logSend(`[defaultModel.updated] ${msg.defaultModel}`);
267
+ break;
268
+
269
+ default:
270
+ logWarn(`Unhandled message type: ${msg.type}`);
271
+ }
272
+ }
273
+
274
+ // ── User message reply ───────────────────────────────────────────────
275
+
276
+ async function handleUserMessage(msg) {
277
+ const replyText = `Mock reply: ${msg.text}`;
278
+
279
+ await sleep(DELAY_MS);
280
+
281
+ if (STREAMING) {
282
+ const runId = randomUUID();
283
+ send({ type: "agent.stream.start", sessionKey: msg.sessionKey, runId });
284
+
285
+ const words = replyText.split(" ");
286
+ for (let i = 0; i < words.length; i++) {
287
+ await sleep(50);
288
+ const chunk = (i === 0 ? "" : " ") + words[i];
289
+ send({ type: "agent.stream.chunk", sessionKey: msg.sessionKey, runId, text: chunk });
290
+ }
291
+
292
+ send({ type: "agent.stream.end", sessionKey: msg.sessionKey, runId });
293
+ logSend(`[agent.stream] ${words.length} chunks`);
294
+
295
+ send({
296
+ type: "agent.text",
297
+ sessionKey: msg.sessionKey,
298
+ text: replyText,
299
+ messageId: randomUUID(),
300
+ });
301
+ } else {
302
+ send({
303
+ type: "agent.text",
304
+ sessionKey: msg.sessionKey,
305
+ text: replyText,
306
+ messageId: randomUUID(),
307
+ });
308
+ logSend(`[agent.text] "${truncate(replyText, 60)}"`);
309
+ }
310
+ }
311
+
312
+ // ── Task run simulation ──────────────────────────────────────────────
313
+
314
+ async function handleTaskRun(msg) {
315
+ const jobId = `mock_job_${Date.now()}`;
316
+ const sessionKey = `agent:${msg.agentId || "main"}:botschat:${userId}:task:mock`;
317
+ const startedAt = Math.floor(Date.now() / 1000);
318
+
319
+ send({
320
+ type: "job.update",
321
+ cronJobId: msg.cronJobId,
322
+ jobId,
323
+ sessionKey,
324
+ status: "running",
325
+ startedAt,
326
+ });
327
+ logSend(`[job.update] running jobId=${jobId}`);
328
+
329
+ send({ type: "job.output", cronJobId: msg.cronJobId, jobId, text: "Mock job started…\n" });
330
+ await sleep(1000);
331
+ send({ type: "job.output", cronJobId: msg.cronJobId, jobId, text: "Mock job processing…\n" });
332
+ await sleep(1000);
333
+ send({ type: "job.output", cronJobId: msg.cronJobId, jobId, text: "Mock job complete.\n" });
334
+
335
+ const finishedAt = Math.floor(Date.now() / 1000);
336
+ send({
337
+ type: "job.update",
338
+ cronJobId: msg.cronJobId,
339
+ jobId,
340
+ sessionKey,
341
+ status: "ok",
342
+ summary: "Mock task executed successfully",
343
+ startedAt,
344
+ finishedAt,
345
+ durationMs: (finishedAt - startedAt) * 1000,
346
+ });
347
+ logSend(`[job.update] ok (${finishedAt - startedAt}s)`);
348
+ }
349
+
350
+ // ── Utilities ────────────────────────────────────────────────────────
351
+
352
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
353
+ function truncate(s, n) { return s && s.length > n ? s.slice(0, n) + "…" : s; }
354
+
355
+ // ── Graceful shutdown ────────────────────────────────────────────────
356
+
357
+ function shutdown() {
358
+ logInfo("Shutting down…");
359
+ intentionalClose = true;
360
+ stopPing();
361
+ ws?.close(1000, "shutdown");
362
+ process.exit(0);
363
+ }
364
+ process.on("SIGINT", shutdown);
365
+ process.on("SIGTERM", shutdown);
366
+
367
+ // ── Start ────────────────────────────────────────────────────────────
368
+
369
+ console.log(`
370
+ ${c.cyan}╭──────────────────────────────────────╮
371
+ │ Mock OpenClaw v1.0 │
372
+ │ Local testing without deployment │
373
+ ╰──────────────────────────────────────╯${c.reset}
374
+ Server: ${SERVER_URL}
375
+ Token: ${TOKEN.slice(0, 12)}***
376
+ Agents: ${AGENTS.join(", ")}
377
+ Model: ${MODEL}
378
+ Delay: ${DELAY_MS}ms
379
+ Streaming: ${STREAMING}
380
+ `);
381
+
382
+ connect();
@@ -1,14 +1,14 @@
1
1
  import WebSocket from "ws";
2
2
  import { deriveKey, encryptText, decryptText, toBase64, fromBase64 } from "../packages/e2e-crypto/e2e-crypto.js";
3
3
 
4
- const E2E_PWD = "botschat123";
4
+ const E2E_PWD = process.env.TEST_E2E_PASSWORD || "";
5
5
 
6
6
  async function main() {
7
7
  // Login
8
8
  const res = await fetch("http://localhost:8787/api/auth/login", {
9
9
  method: "POST",
10
10
  headers: { "Content-Type": "application/json" },
11
- body: JSON.stringify({ email: "tong@mini.local", password: "botschat123" }),
11
+ body: JSON.stringify({ email: "tong@mini.local", password: process.env.TEST_PASSWORD || "" }),
12
12
  });
13
13
  if (!res.ok) { console.log("Login failed:", res.status); process.exit(1); }
14
14
  const login = await res.json() as { id: string; token: string };
@@ -21,7 +21,7 @@ import assert from "node:assert";
21
21
  const API_BASE = "http://localhost:8787";
22
22
  const E2E_PASSWORD = "e2e-test-2026";
23
23
  const TEST_EMAIL = "tong@mini.local";
24
- const TEST_PASS = "botschat123";
24
+ const TEST_PASS = process.env.TEST_E2E_PASSWORD || "";
25
25
  const SECRET_TEXT = `E2E_TEST_SECRET_${Date.now()}`;
26
26
 
27
27
  async function login(): Promise<{ token: string; userId: string }> {
package/wrangler.toml CHANGED
@@ -36,6 +36,9 @@ new_sqlite_classes = ["ConnectionDO"]
36
36
  ENVIRONMENT = "production"
37
37
  # Firebase project ID for Google/GitHub Sign-In token verification.
38
38
  FIREBASE_PROJECT_ID = "botschat-130ff"
39
+ # Google OAuth Client IDs — used to verify native iOS/Android Google Sign-In tokens.
40
+ GOOGLE_WEB_CLIENT_ID = "670756745871-1anrnjukhhu0a550j86blm771a2k3jou.apps.googleusercontent.com"
41
+ GOOGLE_IOS_CLIENT_ID = "670756745871-ct1k370fk3h5oj3f3gdabqv1nq2b7r54.apps.googleusercontent.com"
39
42
  # IMPORTANT: For production, set JWT_SECRET via `wrangler secret put JWT_SECRET`.
40
43
  # The app will refuse to start in non-development mode without it.
41
44