openclacky 1.0.4 → 1.0.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +134 -12
- data/lib/clacky/default_skills/channel-setup/discord_setup.rb +199 -0
- data/lib/clacky/providers.rb +29 -4
- data/lib/clacky/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/clacky/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/clacky/server/channel/adapters/discord/gateway_client.rb +272 -0
- data/lib/clacky/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/clacky/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/clacky/server/channel/channel_config.rb +11 -0
- data/lib/clacky/server/channel.rb +2 -0
- data/lib/clacky/server/http_server.rb +28 -2
- data/lib/clacky/ui2/ui_controller.rb +2 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +44 -0
- data/lib/clacky/web/channels.js +16 -0
- data/lib/clacky/web/i18n.js +24 -2
- data/lib/clacky/web/index.html +6 -1
- data/lib/clacky/web/settings.js +4 -0
- data/lib/clacky/web/version.js +52 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 597ea8fc84ca69641339dbacf724e114d4fc7c5c43bcafb81767bcb83da89edd
|
|
4
|
+
data.tar.gz: 23f576f36023978f37dfe68e12ec942e3cedd591bec1cd098a5829a8d9057a24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0fed807f559eee7d3df5508bbef61d45f75a0943c5d76ac17cbf9a334bfc75be729ad6ab03485211c532de186b1f41aa27c54267f2e36efa7c191699e3fcca2e
|
|
7
|
+
data.tar.gz: c8943553c4cd0f721515e0ffad5aa554dcdffc0305aca6d7ac403a02762b59db63273415d452965767b0bd2157ab835c946b081e99b0ce5a8574a3bf622e6aa1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.5] - 2026-05-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Telegram channel adapter.** New IM channel adapter that connects openclacky to Telegram via the Bot API. Setup is just a bot token from @BotFather — no browser automation, no QR. Mirrors the existing Feishu / WeCom / Weixin contract: HTTPS long-poll inbound, `sendMessage` / `sendPhoto` / `sendDocument` outbound, photo + document download routed through the standard FileProcessor + vision pipeline, group `@-mention` filtering and `allowed_users` whitelist. `base_url` is configurable to support self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) for networks where `api.telegram.org` is unreachable. Frontend Channels panel, `channel-setup` skill, English/Chinese i18n, and `app.css` logo class added. 32 new specs in `spec/clacky/server/channel/adapters/telegram/`.
|
|
12
|
+
- **Discord channel adapter.** Full Discord integration via REST API + Gateway (WebSocket), with channel-setup support, Web UI Channels panel entry, and i18n strings. Connect Clacky to Discord servers for bot interactions through slash commands and message events.
|
|
13
|
+
- **OpenRouter curated model list.** The OpenRouter provider now ships with a curated dropdown of mainstream Claude and GPT models (Sonnet, Opus, Haiku, GPT-5.5/5.4), so users can pick from the list instead of typing model IDs manually. Full catalogue still accessible by typing any model ID.
|
|
14
|
+
- **OpenRouter lite model pairing.** Subagents on OpenRouter now automatically get a sensible cheap/fast sidekick — Claude family pairs with Haiku, GPT family pairs with the mini variant — matching the behavior already available on the native OpenAI and OpenClacky providers.
|
|
15
|
+
- **MiMo 2.5 Pro (Xiaomi) model support.** Added `mimo-v2.5-pro` to the MiMo provider preset alongside existing MiMo models.
|
|
16
|
+
- **AI key setup guide link.** New users and those configuring API keys now see a "New to AI keys? See the guide →" link on both onboarding and settings pages, pointing to the official documentation.
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
- **Default model upgraded to claude-sonnet-4-6.** The OpenClacky provider now defaults to the latest Claude Sonnet model for better performance out of the box.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Linux server restart stability.** Fixed an inherited socket cleanup bug where WEBrick's shutdown would propagate `SHUT_RDWR` to the shared kernel socket, breaking subsequent `accept()` calls on Linux. The server now detaches inherited sockets before shutdown so worker restarts work reliably.
|
|
23
|
+
- **Upgrade failure recovery UI.** When an in-app upgrade restart fails, the UI now shows both tray icon and CLI recovery paths (`gem update ...`) instead of leaving users stranded. Also added branded CLI command info to the version check API for white-label builds.
|
|
24
|
+
|
|
8
25
|
## [1.0.4] - 2026-05-11
|
|
9
26
|
|
|
10
27
|
### Added
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: channel-setup
|
|
3
3
|
description: |
|
|
4
|
-
Configure IM platform channels (Feishu, WeCom, Weixin) for openclacky.
|
|
4
|
+
Configure IM platform channels (Feishu, WeCom, Weixin, Discord, Telegram) for openclacky.
|
|
5
5
|
Uses browser automation for navigation; guides the user to paste credentials and perform UI steps.
|
|
6
|
-
Trigger on: "channel setup", "setup feishu", "setup wecom", "setup weixin", "setup wechat", "
|
|
7
|
-
"channel status", "channel enable", "channel disable", "channel reconfigure", "channel doctor",
|
|
8
|
-
"send message to weixin", "send message to feishu", "send message to wecom".
|
|
6
|
+
Trigger on: "channel setup", "setup feishu", "setup wecom", "setup weixin", "setup wechat", "setup discord", "setup telegram",
|
|
7
|
+
"channel config", "channel status", "channel enable", "channel disable", "channel reconfigure", "channel doctor",
|
|
8
|
+
"send message to weixin", "send message to feishu", "send message to wecom", "send message to discord", "send message to telegram".
|
|
9
9
|
Subcommands: setup, status, enable <platform>, disable <platform>, reconfigure, doctor, send.
|
|
10
10
|
argument-hint: "setup | status | enable <platform> | disable <platform> | reconfigure | doctor | send <platform> <message>"
|
|
11
11
|
allowed-tools:
|
|
@@ -28,13 +28,13 @@ Configure IM platform channels for openclacky.
|
|
|
28
28
|
|
|
29
29
|
| User says | Subcommand |
|
|
30
30
|
|---|---|
|
|
31
|
-
| `channel setup`, `setup feishu`, `setup wecom`, `setup weixin`, `setup wechat` | setup |
|
|
31
|
+
| `channel setup`, `setup feishu`, `setup wecom`, `setup weixin`, `setup wechat`, `setup discord`, `setup telegram` | setup |
|
|
32
32
|
| `channel status` | status |
|
|
33
|
-
| `channel enable feishu/wecom/weixin` | enable |
|
|
34
|
-
| `channel disable feishu/wecom/weixin` | disable |
|
|
33
|
+
| `channel enable feishu/wecom/weixin/discord/telegram` | enable |
|
|
34
|
+
| `channel disable feishu/wecom/weixin/discord/telegram` | disable |
|
|
35
35
|
| `channel reconfigure` | reconfigure |
|
|
36
36
|
| `channel doctor` | doctor |
|
|
37
|
-
| `send <message> to weixin/feishu/wecom` | send |
|
|
37
|
+
| `send <message> to weixin/feishu/wecom/discord/telegram` | send |
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
@@ -51,7 +51,9 @@ Response shape (example):
|
|
|
51
51
|
{"channels":[
|
|
52
52
|
{"platform":"feishu","enabled":true,"running":true,"has_config":true,"app_id":"cli_xxx","domain":"https://open.feishu.cn","allowed_users":[]},
|
|
53
53
|
{"platform":"wecom","enabled":false,"running":false,"has_config":false,"bot_id":""},
|
|
54
|
-
{"platform":"weixin","enabled":true,"running":true,"has_config":true,"has_token":true,"base_url":"https://ilinkai.weixin.qq.com","allowed_users":[]}
|
|
54
|
+
{"platform":"weixin","enabled":true,"running":true,"has_config":true,"has_token":true,"base_url":"https://ilinkai.weixin.qq.com","allowed_users":[]},
|
|
55
|
+
{"platform":"discord","enabled":true,"running":true,"has_config":true,"has_token":true,"allowed_users":[]}
|
|
56
|
+
{"platform":"telegram","enabled":true,"running":true,"has_config":true,"has_token":true,"base_url":"https://api.telegram.org","parse_mode":"Markdown","allowed_users":[]}
|
|
55
57
|
]}
|
|
56
58
|
```
|
|
57
59
|
|
|
@@ -64,12 +66,16 @@ Platform Enabled Running Details
|
|
|
64
66
|
feishu ✅ yes ✅ yes app_id: cli_xxx...
|
|
65
67
|
wecom ❌ no ❌ no (not configured)
|
|
66
68
|
weixin ✅ yes ✅ yes has_token: true
|
|
69
|
+
discord ✅ yes ✅ yes has_token: true
|
|
70
|
+
telegram ✅ yes ✅ yes has_token: true
|
|
67
71
|
─────────────────────────────────────────────────────
|
|
68
72
|
```
|
|
69
73
|
|
|
70
74
|
- Feishu: show `app_id` (truncated to 12 chars)
|
|
71
75
|
- WeCom: show `bot_id` if present
|
|
72
76
|
- Weixin: show `has_token: true/false` (token value is never displayed)
|
|
77
|
+
- Discord: show `has_token: true/false` (token value is never displayed)
|
|
78
|
+
- Telegram: show `has_token: true/false` (bot token is never displayed)
|
|
73
79
|
|
|
74
80
|
If the API is unreachable or returns an empty list: "No channels configured yet. Run `/channel-setup setup` to get started."
|
|
75
81
|
|
|
@@ -83,6 +89,8 @@ Ask:
|
|
|
83
89
|
> 1. Feishu
|
|
84
90
|
> 2. WeCom (Enterprise WeChat)
|
|
85
91
|
> 3. Weixin (Personal WeChat via iLink QR login)
|
|
92
|
+
> 4. Discord
|
|
93
|
+
> 5. Telegram (Bot API)
|
|
86
94
|
|
|
87
95
|
---
|
|
88
96
|
|
|
@@ -344,6 +352,105 @@ Tell the user while waiting:
|
|
|
344
352
|
|
|
345
353
|
---
|
|
346
354
|
|
|
355
|
+
### Discord setup
|
|
356
|
+
|
|
357
|
+
Discord requires manual portal interaction (hCaptcha gates Application creation). The browser just navigates the user to the portal; the user clicks through and pastes the bot token + app id back.
|
|
358
|
+
|
|
359
|
+
#### Step 1 — Open the developer portal
|
|
360
|
+
|
|
361
|
+
Get the portal URL from the script and open it in the browser:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
PORTAL_URL=$(ruby "SKILL_DIR/discord_setup.rb" --portal-url)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Open it: `browser(action="navigate", url="<PORTAL_URL>")`. If the browser tool is not configured, invoke `browser-setup` first, then retry.
|
|
368
|
+
|
|
369
|
+
#### Step 2 — Guide the user through the portal (one round-trip)
|
|
370
|
+
|
|
371
|
+
Tell the user **all** of the following in a single message, then call `request_user_feedback` to collect the values in one reply:
|
|
372
|
+
|
|
373
|
+
> In the Discord Developer Portal I just opened:
|
|
374
|
+
>
|
|
375
|
+
> 1. Click **New Application** (top-right). Name it whatever you like (e.g. "Open Clacky"), check the ToS box, click **Create**.
|
|
376
|
+
> 2. In the left nav click **Bot**.
|
|
377
|
+
> 3. Scroll down to **Privileged Gateway Intents** and turn on **MESSAGE CONTENT INTENT**, then click **Save Changes**.
|
|
378
|
+
> 4. Scroll up, click **Reset Token** → **Yes, do it!**. Click **Copy** to copy the bot token. (This is the only time the token is shown — don't navigate away before copying.)
|
|
379
|
+
> 5. In the left nav click **General Information**. Copy the **Application ID**.
|
|
380
|
+
>
|
|
381
|
+
> Paste both values back here in this format (one line):
|
|
382
|
+
>
|
|
383
|
+
> `token=YOUR_BOT_TOKEN app_id=YOUR_APPLICATION_ID`
|
|
384
|
+
|
|
385
|
+
If the user is chatting in a non-English language, append the localized label in parens after each bolded English button name (e.g. `**Bot**(机器人)`). The English label stays primary — it's what they physically click in the portal.
|
|
386
|
+
|
|
387
|
+
Use `request_user_feedback` to collect the reply. Parse with tolerant regex (`token=\S+`, `app_id=\d+`).
|
|
388
|
+
|
|
389
|
+
If the reply is malformed (missing either field), apologise briefly and ask again with the exact same format reminder. Up to 3 retries; after that, surface the original message and stop.
|
|
390
|
+
|
|
391
|
+
#### Step 3 — Validate, save, invite, wait
|
|
392
|
+
|
|
393
|
+
1. Validate the token and save credentials:
|
|
394
|
+
```bash
|
|
395
|
+
ruby "SKILL_DIR/discord_setup.rb" --validate "<BOT_TOKEN>"
|
|
396
|
+
```
|
|
397
|
+
On success the script prints `{"bot_id":"...","username":"..."}` and the adapter starts.
|
|
398
|
+
|
|
399
|
+
2. Generate the invite URL using the application id from Step 2:
|
|
400
|
+
```bash
|
|
401
|
+
ruby "SKILL_DIR/discord_setup.rb" --invite-url "<APP_ID>"
|
|
402
|
+
```
|
|
403
|
+
Open it: `browser(action="navigate", url="<INVITE_URL>")`. Tell the user:
|
|
404
|
+
> Pick your server from the dropdown → **Continue** → **Authorize**. I'll detect when the bot joins.
|
|
405
|
+
>
|
|
406
|
+
> If the dropdown is empty, you don't have a server yet — open <https://discord.com/channels/@me>, click **Add a Server** (the **+** button on the left sidebar) → **Create My Own** → **For me and my friends** → name it → **Create**, then re-open the invite link.
|
|
407
|
+
|
|
408
|
+
3. Wait for the bot to join a guild (long-poll, 10 min timeout). Run with `timeout: 620`:
|
|
409
|
+
```bash
|
|
410
|
+
ruby "SKILL_DIR/discord_setup.rb" --watch-guild
|
|
411
|
+
```
|
|
412
|
+
On exit 0: "✅ Discord channel configured! Bot is in `<guild_name>`. Mention it or DM it from any channel."
|
|
413
|
+
On timeout: offer to re-open the invite URL — the bot token stays valid.
|
|
414
|
+
|
|
415
|
+
### Telegram setup (Bot API)
|
|
416
|
+
|
|
417
|
+
Telegram setup is by far the simplest — no browser automation, no QR. The user creates a bot via @BotFather and pastes the token here.
|
|
418
|
+
|
|
419
|
+
#### Step 1 — Create a bot via @BotFather
|
|
420
|
+
|
|
421
|
+
Tell the user:
|
|
422
|
+
|
|
423
|
+
> Open Telegram and start a chat with **@BotFather** (https://t.me/BotFather). Send `/newbot`, choose a display name and a username ending in `bot`. BotFather will reply with an HTTP API token that looks like `123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ`. Paste the token here.
|
|
424
|
+
>
|
|
425
|
+
> Optional: if your network blocks `api.telegram.org`, also tell me the base URL of your self-hosted Bot API server (e.g. `https://my-tg-proxy.example.com`). Otherwise leave it blank.
|
|
426
|
+
|
|
427
|
+
Wait for the user's reply. Parse the token (matches `^\d+:[\w-]{30,}$`).
|
|
428
|
+
|
|
429
|
+
#### Step 2 — Save credentials and validate
|
|
430
|
+
|
|
431
|
+
Call the server API. It calls `getMe` against the Bot API to validate the token before persisting:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/telegram \
|
|
435
|
+
-H "Content-Type: application/json" \
|
|
436
|
+
-d '{"bot_token":"<TOKEN>","base_url":"<BASE_URL_OR_OMIT>"}'
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
- `200 { "ok": true }` — token validated and saved. The adapter starts long-polling immediately.
|
|
440
|
+
- `422 { "ok": false, "error": "..." }` — show the error (commonly "Unauthorized" → wrong token) and offer to retry.
|
|
441
|
+
|
|
442
|
+
On success:
|
|
443
|
+
|
|
444
|
+
> ✅ Telegram channel configured. Open your bot in Telegram and send any message to start chatting. In group chats, the bot only replies when you @-mention it.
|
|
445
|
+
|
|
446
|
+
#### Notes
|
|
447
|
+
|
|
448
|
+
- **Group chats**: Telegram bots respond only when @-mentioned or directly replied to (matches Feishu behaviour). For unrestricted reading in groups, the bot owner must disable "Group Privacy" in @BotFather (`/mybots → Bot Settings → Group Privacy → Turn off`), but `@-mention only` is recommended to avoid spam.
|
|
449
|
+
- **Self-hosted Bot API**: set `base_url` when `api.telegram.org` is unreachable. See https://github.com/tdlib/telegram-bot-api for the official self-hosted server.
|
|
450
|
+
- **`allowed_users`**: restrict which Telegram user IDs the bot will respond to. Find a user's numeric ID by messaging @userinfobot.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
347
454
|
## `enable`
|
|
348
455
|
|
|
349
456
|
Call the server API to re-enable the platform (this reads from disk, sets enabled, saves, and hot-reloads):
|
|
@@ -391,15 +498,30 @@ Check each item, report ✅ / ❌ with remediation:
|
|
|
391
498
|
- Feishu: `app_id`, `app_secret` present and non-empty
|
|
392
499
|
- WeCom: `bot_id`, `secret` present and non-empty
|
|
393
500
|
- Weixin: `token` present and non-empty in `channels.yml`
|
|
501
|
+
- Discord: `bot_token` present and non-empty in `channels.yml`
|
|
502
|
+
- Telegram: `bot_token` present and non-empty
|
|
394
503
|
3. **Feishu credentials** (if enabled) — run the token API call, check `code=0`.
|
|
395
504
|
4. **Weixin token** (if enabled) — call `GET /api/channels` and check `has_token: true` for the weixin entry.
|
|
396
|
-
5. **
|
|
505
|
+
5. **Telegram credentials** (if enabled) — call `getMe` against the Bot API:
|
|
506
|
+
```bash
|
|
507
|
+
BOT_TOKEN=$(ruby -ryaml -e 'puts (YAML.load_file(File.expand_path("~/.clacky/channels.yml"))["channels"]["telegram"]["bot_token"] rescue "")')
|
|
508
|
+
BASE_URL=$(ruby -ryaml -e 'puts (YAML.load_file(File.expand_path("~/.clacky/channels.yml"))["channels"]["telegram"]["base_url"] || "https://api.telegram.org" rescue "https://api.telegram.org")')
|
|
509
|
+
curl -s "$BASE_URL/bot$BOT_TOKEN/getMe" | grep -q '"ok":true' && echo "✅ Telegram OK" || echo "❌ Telegram credentials rejected by getMe"
|
|
510
|
+
```
|
|
511
|
+
6. **WeCom credentials** (if enabled) — search today's log:
|
|
397
512
|
```bash
|
|
398
513
|
grep -iE "wecom adapter loop started|WeCom authentication failed|WeCom WS error response|WecomAdapter" \
|
|
399
514
|
~/.clacky/logger/clacky-$(date +%Y-%m-%d).log
|
|
400
515
|
```
|
|
401
516
|
- `WeCom authentication failed` or non-zero errcode → ❌ "WeCom credentials incorrect"
|
|
402
517
|
- `adapter loop started` with no auth error → ✅
|
|
518
|
+
6. **Discord credentials** (if enabled) — call `GET /api/channels` and check `has_token: true`. Search today's log:
|
|
519
|
+
```bash
|
|
520
|
+
grep -iE "DiscordAdapter|discord-gateway|/users/@me failed" \
|
|
521
|
+
~/.clacky/logger/clacky-$(date +%Y-%m-%d).log
|
|
522
|
+
```
|
|
523
|
+
- `/users/@me failed` → ❌ "Discord token invalid or revoked — re-run setup"
|
|
524
|
+
- `authenticated as` with no error → ✅
|
|
403
525
|
|
|
404
526
|
---
|
|
405
527
|
|
|
@@ -410,7 +532,7 @@ Proactively send a message to a user via an IM channel adapter.
|
|
|
410
532
|
### Parse the request
|
|
411
533
|
|
|
412
534
|
Extract two things from the user's instruction:
|
|
413
|
-
- **platform** — one of `weixin`, `feishu`, `wecom`
|
|
535
|
+
- **platform** — one of `weixin`, `feishu`, `wecom`, `discord`, `telegram`
|
|
414
536
|
- **message** — the text content to send
|
|
415
537
|
|
|
416
538
|
If the platform cannot be inferred, ask the user to clarify.
|
|
@@ -450,7 +572,7 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/
|
|
|
450
572
|
### Constraints & notes
|
|
451
573
|
|
|
452
574
|
- **Weixin (iLink protocol)**: Every outbound message requires a `context_token` that is obtained from the most recent inbound message from that user. The token is cached in memory and reset on server restart. If the server was restarted since the user last wrote, the token is gone and the send will fail — the user must message the bot again.
|
|
453
|
-
- **Feishu / WeCom**: No token required. As long as the adapter is running and the `user_id` / `chat_id` is valid, the message will be delivered.
|
|
575
|
+
- **Feishu / WeCom / Discord / Telegram**: No per-message token required. As long as the adapter is running and the `user_id` / `chat_id` (or Discord channel/user id) is valid, the message will be delivered. For Telegram specifically, the `user_id` must be a Telegram chat_id that the bot can write to — the user must have sent at least one message to the bot first.
|
|
454
576
|
- This feature is intended for **proactive notifications** (e.g. task completion, reminders). It is not a replacement for the normal reply flow triggered by inbound messages.
|
|
455
577
|
|
|
456
578
|
---
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# discord_setup.rb — Discord channel setup helper.
|
|
5
|
+
#
|
|
6
|
+
# Discord's developer portal requires manual interaction (hCaptcha + private API), so the
|
|
7
|
+
# Agent uses the browser only as a container — it navigates to the portal and the user
|
|
8
|
+
# creates the App manually, then pastes back the bot token and application id. This
|
|
9
|
+
# script handles everything a shell can do: emit the portal URL, validate the token
|
|
10
|
+
# against /users/@me, save to the clacky server, generate the OAuth2 invite URL, and
|
|
11
|
+
# poll until the bot is in at least one guild.
|
|
12
|
+
#
|
|
13
|
+
# Modes:
|
|
14
|
+
# --portal-url Print the Discord developer portal URL (stdout, single line)
|
|
15
|
+
# --validate <token> Validate bot_token via /users/@me, then POST to server
|
|
16
|
+
# --invite-url <client_id> Print the OAuth2 invite URL (stdout, single line)
|
|
17
|
+
# --watch-guild Long-poll /users/@me/guilds via the saved token
|
|
18
|
+
# until at least one guild appears (or timeout)
|
|
19
|
+
# --bot-info <token> Print {id, username} JSON for an unsaved token
|
|
20
|
+
#
|
|
21
|
+
# Environment:
|
|
22
|
+
# CLACKY_SERVER_HOST default 127.0.0.1
|
|
23
|
+
# CLACKY_SERVER_PORT default 7070
|
|
24
|
+
|
|
25
|
+
require "json"
|
|
26
|
+
require "net/http"
|
|
27
|
+
require "net/https"
|
|
28
|
+
require "uri"
|
|
29
|
+
require "openssl"
|
|
30
|
+
require "cgi"
|
|
31
|
+
require "yaml"
|
|
32
|
+
|
|
33
|
+
DISCORD_API_BASE = "https://discord.com/api/v10"
|
|
34
|
+
DISCORD_OAUTH_BASE = "https://discord.com/oauth2/authorize"
|
|
35
|
+
DISCORD_PORTAL_URL = "https://discord.com/developers/applications"
|
|
36
|
+
DEFAULT_BOT_PERMS = "274877990912"
|
|
37
|
+
DEFAULT_BOT_SCOPES = "bot applications.commands"
|
|
38
|
+
WATCH_GUILD_DEADLINE = 10 * 60
|
|
39
|
+
WATCH_GUILD_INTERVAL = 3
|
|
40
|
+
USER_AGENT = "DiscordBot (https://github.com/clackyai/openclacky, 1.0)"
|
|
41
|
+
|
|
42
|
+
CLACKY_SERVER_URL = begin
|
|
43
|
+
host = ENV.fetch("CLACKY_SERVER_HOST", "127.0.0.1")
|
|
44
|
+
port = ENV.fetch("CLACKY_SERVER_PORT", "7070")
|
|
45
|
+
"http://#{host}:#{port}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def step(msg); $stderr.puts("[discord-setup] #{msg}"); end
|
|
49
|
+
def ok(msg); $stderr.puts("[discord-setup] #{msg}"); end
|
|
50
|
+
def warn!(msg); $stderr.puts("[discord-setup] #{msg}"); end
|
|
51
|
+
|
|
52
|
+
def fail!(msg, json: false)
|
|
53
|
+
if json
|
|
54
|
+
$stdout.puts(JSON.generate({ error: msg }))
|
|
55
|
+
else
|
|
56
|
+
$stderr.puts("[discord-setup] #{msg}")
|
|
57
|
+
end
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def discord_get(path, bot_token:, timeout: 15)
|
|
62
|
+
uri = URI("#{DISCORD_API_BASE}#{path}")
|
|
63
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
64
|
+
http.use_ssl = true
|
|
65
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
66
|
+
http.read_timeout = timeout
|
|
67
|
+
http.open_timeout = 10
|
|
68
|
+
|
|
69
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
70
|
+
req["Authorization"] = "Bot #{bot_token}"
|
|
71
|
+
req["User-Agent"] = USER_AGENT
|
|
72
|
+
req["Accept"] = "application/json"
|
|
73
|
+
|
|
74
|
+
res = http.request(req)
|
|
75
|
+
body = res.body.to_s
|
|
76
|
+
parsed = (JSON.parse(body) rescue nil)
|
|
77
|
+
|
|
78
|
+
unless res.is_a?(Net::HTTPSuccess)
|
|
79
|
+
msg = parsed.is_a?(Hash) ? (parsed["message"] || body.slice(0, 200)) : body.slice(0, 200)
|
|
80
|
+
raise "Discord HTTP #{res.code} #{path}: #{msg}"
|
|
81
|
+
end
|
|
82
|
+
parsed
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def saved_bot_token
|
|
86
|
+
yml_path = File.expand_path("~/.clacky/channels.yml")
|
|
87
|
+
return nil unless File.exist?(yml_path)
|
|
88
|
+
data = YAML.safe_load_file(yml_path, permitted_classes: [Symbol], aliases: true) rescue nil
|
|
89
|
+
data&.dig("channels", "discord", "bot_token") || data&.dig(:channels, :discord, :bot_token)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def save_to_server(bot_token:)
|
|
93
|
+
uri = URI("#{CLACKY_SERVER_URL}/api/channels/discord")
|
|
94
|
+
body = JSON.generate({ bot_token: bot_token })
|
|
95
|
+
|
|
96
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
97
|
+
http.read_timeout = 30
|
|
98
|
+
http.open_timeout = 5
|
|
99
|
+
|
|
100
|
+
req = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
|
|
101
|
+
req.body = body
|
|
102
|
+
|
|
103
|
+
res = http.request(req)
|
|
104
|
+
data = JSON.parse(res.body) rescue {}
|
|
105
|
+
|
|
106
|
+
unless res.is_a?(Net::HTTPSuccess) && data["ok"]
|
|
107
|
+
fail!("Failed to save Discord config: #{data["error"] || res.body.slice(0, 200)}")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
mode_idx = ARGV.index { |a| a.start_with?("--") }
|
|
112
|
+
mode = mode_idx ? ARGV[mode_idx] : nil
|
|
113
|
+
arg = mode_idx ? ARGV[mode_idx + 1] : nil
|
|
114
|
+
|
|
115
|
+
case mode
|
|
116
|
+
when "--portal-url"
|
|
117
|
+
$stdout.puts(DISCORD_PORTAL_URL)
|
|
118
|
+
exit 0
|
|
119
|
+
|
|
120
|
+
when "--validate"
|
|
121
|
+
fail!("--validate requires <bot_token>") if arg.to_s.strip.empty?
|
|
122
|
+
bot_token = arg.strip
|
|
123
|
+
step("Validating bot token against Discord API...")
|
|
124
|
+
begin
|
|
125
|
+
me = discord_get("/users/@me", bot_token: bot_token)
|
|
126
|
+
rescue => e
|
|
127
|
+
fail!("Token validation failed: #{e.message}")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
bot_id = me["id"].to_s
|
|
131
|
+
username = me["username"].to_s
|
|
132
|
+
fail!("Empty bot id from /users/@me") if bot_id.empty?
|
|
133
|
+
|
|
134
|
+
ok("Authenticated as #{username} (id=#{bot_id})")
|
|
135
|
+
step("Saving credentials via clacky server...")
|
|
136
|
+
save_to_server(bot_token: bot_token)
|
|
137
|
+
ok("Discord channel configured")
|
|
138
|
+
|
|
139
|
+
$stdout.puts(JSON.generate({ bot_id: bot_id, username: username }))
|
|
140
|
+
exit 0
|
|
141
|
+
|
|
142
|
+
when "--bot-info"
|
|
143
|
+
fail!("--bot-info requires <bot_token>", json: true) if arg.to_s.strip.empty?
|
|
144
|
+
begin
|
|
145
|
+
me = discord_get("/users/@me", bot_token: arg.strip)
|
|
146
|
+
rescue => e
|
|
147
|
+
fail!(e.message, json: true)
|
|
148
|
+
end
|
|
149
|
+
$stdout.puts(JSON.generate({ bot_id: me["id"], username: me["username"] }))
|
|
150
|
+
exit 0
|
|
151
|
+
|
|
152
|
+
when "--invite-url"
|
|
153
|
+
fail!("--invite-url requires <client_id>") if arg.to_s.strip.empty?
|
|
154
|
+
client_id = arg.strip
|
|
155
|
+
url = "#{DISCORD_OAUTH_BASE}?client_id=#{CGI.escape(client_id)}" \
|
|
156
|
+
"&permissions=#{DEFAULT_BOT_PERMS}" \
|
|
157
|
+
"&scope=#{CGI.escape(DEFAULT_BOT_SCOPES)}"
|
|
158
|
+
$stdout.puts(url)
|
|
159
|
+
exit 0
|
|
160
|
+
|
|
161
|
+
when "--watch-guild"
|
|
162
|
+
bot_token = saved_bot_token
|
|
163
|
+
fail!("No saved bot_token in ~/.clacky/channels.yml — run --validate first") if bot_token.to_s.empty?
|
|
164
|
+
|
|
165
|
+
step("Waiting for the bot to be added to a guild (timeout: #{WATCH_GUILD_DEADLINE / 60} min)...")
|
|
166
|
+
deadline = Time.now + WATCH_GUILD_DEADLINE
|
|
167
|
+
|
|
168
|
+
loop do
|
|
169
|
+
fail!("Timed out waiting for the bot to join a guild. Open the invite URL again to retry.") if Time.now > deadline
|
|
170
|
+
|
|
171
|
+
begin
|
|
172
|
+
guilds = discord_get("/users/@me/guilds", bot_token: bot_token)
|
|
173
|
+
rescue => e
|
|
174
|
+
warn!("Poll error (will retry): #{e.message}")
|
|
175
|
+
sleep WATCH_GUILD_INTERVAL
|
|
176
|
+
next
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
if guilds.is_a?(Array) && !guilds.empty?
|
|
180
|
+
g = guilds.first
|
|
181
|
+
ok("Bot added to guild: #{g["name"]} (id=#{g["id"]})")
|
|
182
|
+
$stdout.puts(JSON.generate({ guild_id: g["id"], guild_name: g["name"], total: guilds.length }))
|
|
183
|
+
exit 0
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
sleep WATCH_GUILD_INTERVAL
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
else
|
|
190
|
+
$stderr.puts(<<~USAGE)
|
|
191
|
+
Usage:
|
|
192
|
+
ruby discord_setup.rb --portal-url
|
|
193
|
+
ruby discord_setup.rb --validate <bot_token>
|
|
194
|
+
ruby discord_setup.rb --bot-info <bot_token>
|
|
195
|
+
ruby discord_setup.rb --invite-url <client_id>
|
|
196
|
+
ruby discord_setup.rb --watch-guild
|
|
197
|
+
USAGE
|
|
198
|
+
exit 1
|
|
199
|
+
end
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -29,7 +29,7 @@ module Clacky
|
|
|
29
29
|
"name" => "OpenClacky",
|
|
30
30
|
"base_url" => "https://api.openclacky.com",
|
|
31
31
|
"api" => "bedrock",
|
|
32
|
-
"default_model" => "abs-claude-sonnet-4-
|
|
32
|
+
"default_model" => "abs-claude-sonnet-4-6",
|
|
33
33
|
"models" => [
|
|
34
34
|
"abs-claude-opus-4-7",
|
|
35
35
|
"abs-claude-opus-4-6",
|
|
@@ -80,7 +80,32 @@ module Clacky
|
|
|
80
80
|
"base_url" => "https://openrouter.ai/api/v1",
|
|
81
81
|
"api" => "openai-responses",
|
|
82
82
|
"default_model" => "anthropic/claude-sonnet-4-6",
|
|
83
|
-
|
|
83
|
+
# Curated default lineup. OpenRouter's full catalogue is enormous
|
|
84
|
+
# (hundreds of models) and the live /models endpoint isn't always
|
|
85
|
+
# reachable from every region — shipping a small list of the
|
|
86
|
+
# mainstream Claude + GPT entries gives users a working dropdown
|
|
87
|
+
# out of the box. Users can still type any other OpenRouter model
|
|
88
|
+
# ID manually; this list only seeds the picker.
|
|
89
|
+
"models" => [
|
|
90
|
+
"anthropic/claude-sonnet-4-6",
|
|
91
|
+
"anthropic/claude-opus-4-7",
|
|
92
|
+
"anthropic/claude-opus-4-6",
|
|
93
|
+
"anthropic/claude-haiku-4-5",
|
|
94
|
+
"openai/gpt-5.5",
|
|
95
|
+
"openai/gpt-5.4",
|
|
96
|
+
"openai/gpt-5.4-mini"
|
|
97
|
+
],
|
|
98
|
+
# Per-primary lite pairing — Claude family pairs with Haiku, GPT
|
|
99
|
+
# family pairs with the mini variant. Mirrors the openclacky and
|
|
100
|
+
# openai presets above so subagents on OpenRouter get a sensible
|
|
101
|
+
# cheap/fast sidekick automatically.
|
|
102
|
+
"lite_models" => {
|
|
103
|
+
"anthropic/claude-sonnet-4-6" => "anthropic/claude-haiku-4-5",
|
|
104
|
+
"anthropic/claude-opus-4-7" => "anthropic/claude-haiku-4-5",
|
|
105
|
+
"anthropic/claude-opus-4-6" => "anthropic/claude-haiku-4-5",
|
|
106
|
+
"openai/gpt-5.5" => "openai/gpt-5.4-mini",
|
|
107
|
+
"openai/gpt-5.4" => "openai/gpt-5.4-mini"
|
|
108
|
+
},
|
|
84
109
|
# Per-model API type overrides. Matched by Regexp against the model name.
|
|
85
110
|
# Why this exists: OpenRouter proxies Claude via both its OpenAI-compatible
|
|
86
111
|
# /chat/completions endpoint AND a native Anthropic /v1/messages endpoint.
|
|
@@ -243,8 +268,8 @@ module Clacky
|
|
|
243
268
|
"name" => "MiMo (Xiaomi)",
|
|
244
269
|
"base_url" => "https://api.xiaomimimo.com/v1",
|
|
245
270
|
"api" => "openai-completions",
|
|
246
|
-
"default_model" => "mimo-v2-pro",
|
|
247
|
-
"models" => ["mimo-v2-pro", "mimo-v2-omni"],
|
|
271
|
+
"default_model" => "mimo-v2.5-pro",
|
|
272
|
+
"models" => ["mimo-v2.5-pro", "mimo-v2-pro", "mimo-v2-omni"],
|
|
248
273
|
# MiMo-V2-Pro is text-only; MiMo-V2-Omni supports vision (omni = multimodal).
|
|
249
274
|
"capabilities" => { "vision" => false }.freeze,
|
|
250
275
|
"model_capabilities" => {
|