openclacky 1.0.3 → 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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -1
  3. data/benchmark/fixtures/sample_project/Gemfile +3 -0
  4. data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
  5. data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
  6. data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
  7. data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
  8. data/benchmark/results/EVALUATION_REPORT.md +165 -0
  9. data/benchmark/results/baseline_20260511_174424.json +128 -0
  10. data/benchmark/results/report_20260511_175256.json +271 -0
  11. data/benchmark/results/report_20260511_175444.json +271 -0
  12. data/benchmark/results/treatment_20260511_175103.json +130 -0
  13. data/benchmark/runner.rb +441 -0
  14. data/docs/proposals/2026-05-11-system-prompt-alignment.md +325 -0
  15. data/docs/proposals/2026-05-12-memory-mechanism-optimization.md +89 -0
  16. data/lib/clacky/agent/cost_tracker.rb +8 -2
  17. data/lib/clacky/agent/memory_updater.rb +41 -30
  18. data/lib/clacky/agent/skill_manager.rb +5 -2
  19. data/lib/clacky/agent/skill_reflector.rb +10 -1
  20. data/lib/clacky/agent.rb +4 -0
  21. data/lib/clacky/client.rb +15 -0
  22. data/lib/clacky/default_agents/base_prompt.md +20 -20
  23. data/lib/clacky/default_agents/coding/system_prompt.md +51 -1
  24. data/lib/clacky/default_skills/channel-setup/SKILL.md +190 -14
  25. data/lib/clacky/default_skills/channel-setup/discord_setup.rb +199 -0
  26. data/lib/clacky/default_skills/channel-setup/import_lark_skills.rb +97 -0
  27. data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
  28. data/lib/clacky/default_skills/persist-memory/SKILL.md +59 -0
  29. data/lib/clacky/providers.rb +77 -10
  30. data/lib/clacky/server/channel/adapters/discord/adapter.rb +229 -0
  31. data/lib/clacky/server/channel/adapters/discord/api_client.rb +107 -0
  32. data/lib/clacky/server/channel/adapters/discord/gateway_client.rb +272 -0
  33. data/lib/clacky/server/channel/adapters/telegram/adapter.rb +375 -0
  34. data/lib/clacky/server/channel/adapters/telegram/api_client.rb +205 -0
  35. data/lib/clacky/server/channel/channel_config.rb +11 -0
  36. data/lib/clacky/server/channel.rb +2 -0
  37. data/lib/clacky/server/http_server.rb +69 -3
  38. data/lib/clacky/ui2/ui_controller.rb +2 -1
  39. data/lib/clacky/utils/file_processor.rb +71 -0
  40. data/lib/clacky/version.rb +1 -1
  41. data/lib/clacky/web/app.css +44 -0
  42. data/lib/clacky/web/channels.js +16 -0
  43. data/lib/clacky/web/i18n.js +24 -2
  44. data/lib/clacky/web/index.html +6 -1
  45. data/lib/clacky/web/settings.js +4 -0
  46. data/lib/clacky/web/version.js +52 -1
  47. metadata +37 -2
@@ -3,7 +3,7 @@ users complete software development projects. You are responsible for developmen
3
3
 
4
4
  Your role is to:
5
5
  - Understand project requirements and translate them into technical solutions
6
- - Write clean, maintainable, and well-documented code
6
+ - Write clean, maintainable code
7
7
  - Follow best practices and industry standards
8
8
  - Explain technical concepts in simple terms when needed
9
9
  - Proactively identify potential issues and suggest improvements
@@ -15,3 +15,53 @@ Working process:
15
15
  3. You should frequently refer to the existing codebase. For unclear instructions,
16
16
  prioritize understanding the codebase first before answering or taking action.
17
17
  Always read relevant code files to understand the project structure, patterns, and conventions.
18
+
19
+ ## Code Style
20
+
21
+ - **Default to writing no comments.** Only add one when the WHY is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, or behavior that would surprise a reader.
22
+ - Don't explain WHAT the code does — well-named identifiers already do that.
23
+ - Don't reference the current task, fix, or callers ("used by X", "added for the Y flow", "handles the case from issue #123"). These belong in the PR description and rot as the codebase evolves.
24
+ - Never write multi-paragraph docstrings or multi-line comment blocks — one short line max.
25
+
26
+ ## File Modification Rules
27
+
28
+ - **ALWAYS prefer `edit` over `write`.** Use `write` only for creating entirely new files or complete rewrites.
29
+ - When editing text from `file_reader` output, preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix.
30
+ - Ensure `old_string` is unique in the file. If not, provide a larger string with more surrounding context to make it unique.
31
+ - Use `replace_all` only when you genuinely need to change every occurrence.
32
+ - When referencing specific functions or pieces of code, include `file_path:line_number` to help the user navigate.
33
+
34
+ ## Git Safety Protocol
35
+
36
+ - NEVER update git config (user.name, user.email, etc.)
37
+ - NEVER run destructive commands: `git push --force`, `git reset --hard`, `git checkout .`, `git clean -f`
38
+ - NEVER skip hooks (`--no-verify`, `--no-gpg-sign`)
39
+ - When staging files, prefer `git add <specific-file>` over `git add -A` or `git add .`
40
+ - Always create NEW commits rather than amending existing ones
41
+ - Never amend published commits
42
+ - Only create commits when requested by the user. If unclear, ask first.
43
+
44
+ ## Error Handling
45
+
46
+ - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees.
47
+ - Only validate at system boundaries (user input, external APIs).
48
+ - Don't use feature flags or backwards-compatibility shims when you can just change the code.
49
+
50
+ ## Security
51
+
52
+ - Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities.
53
+ - If you notice insecure code, immediately fix it.
54
+ - Prioritize writing safe, secure, and correct code.
55
+
56
+ ## Testing
57
+
58
+ - For UI or frontend changes, start the dev server and verify in a browser before reporting the task as complete.
59
+ - Type checking and test suites verify code correctness, not feature correctness — if you can't test the UI, say so explicitly rather than claiming success.
60
+ - When the user asks you to run tests, do so and report the results.
61
+
62
+ ## Code Quality
63
+
64
+ - Don't add features, refactor, or introduce abstractions beyond what the task requires.
65
+ - A bug fix doesn't need surrounding cleanup; a one-shot operation doesn't need a helper.
66
+ - Three similar lines is better than a premature abstraction.
67
+ - No half-finished implementations either.
@@ -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", "channel config",
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
 
@@ -100,7 +108,7 @@ ruby "SKILL_DIR/feishu_setup.rb"
100
108
  - The script completed successfully.
101
109
  - Config is already written to `~/.clacky/channels.yml`.
102
110
  - Tell the user: "✅ Feishu channel configured automatically! The channel is ready."
103
- - **Stop here do not proceed to manual steps.**
111
+ - **Skip Step 2 (manual fallback) and continue to Step 3.**
104
112
 
105
113
  **If exit code is non-0:**
106
114
  - Check stdout for the error message.
@@ -199,7 +207,61 @@ curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/in
199
207
  -d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}'
200
208
  ```
201
209
 
202
- Check for `"code":0`. On success: "✅ Feishu channel configured."
210
+ Check for `"code":0`. On success: continue to Step 3 (below).
211
+
212
+ ##### Phase 9 — done
213
+
214
+ Step 2 ends here. **Continue to Step 3.**
215
+
216
+ ---
217
+
218
+ #### Step 3 — Optional: install Feishu CLI
219
+
220
+ Reach here from either Step 1 success or end of Step 2. Read `app_id` and `app_secret` from `~/.clacky/channels.yml` (under `channels.feishu`) for the install commands below.
221
+
222
+ Call `request_user_feedback`:
223
+
224
+ zh:
225
+ ```json
226
+ {
227
+ "question": "是否要安装「飞书 CLI」?装好之后 AI 可以帮你操作飞书云文档、电子表格、多维表格、知识库、日历、任务等几乎全部飞书能力,不只是聊天,而是能\"做事\"。不装也 OK。",
228
+ "options": ["启用", "跳过"]
229
+ }
230
+ ```
231
+
232
+ en:
233
+ ```json
234
+ {
235
+ "question": "Install Feishu CLI? With it, the AI can operate Feishu Docs, Sheets, Bitable, Wiki, Calendar, Tasks and almost every Feishu capability — not just chat, but actually get things done. Skipping is fine.",
236
+ "options": ["Enable", "Skip"]
237
+ }
238
+ ```
239
+
240
+ If the user picks Skip, stop — setup is complete.
241
+
242
+ If the user picks Enable, run:
243
+
244
+ ```bash
245
+ lark-cli --version > /dev/null 2>&1 || npm install -g @larksuite/cli
246
+ echo -n "<APP_SECRET>" | lark-cli config init --app-id <APP_ID> --app-secret-stdin --brand feishu
247
+ npx -y skills add larksuite/cli -y -g
248
+ ruby "SKILL_DIR/import_lark_skills.rb"
249
+ lark-cli auth login --recommend
250
+ ```
251
+
252
+ The last command blocks up to 10 minutes waiting for browser authorization — make sure the runner's timeout is ≥ 600s.
253
+
254
+ Once you see the authorization URL in the command's stdout, tell the user (do **not** wait for a reply — the CLI's blocking poll will return on its own when authorization completes):
255
+ - zh: "请在浏览器中打开下方链接完成授权:\n<URL>"
256
+ - en: "Open this URL in your browser to authorize:\n<URL>"
257
+
258
+ **Do not kill and restart this command** — restarting invalidates the device code and breaks the link the user already opened. The "hang" is just polling; wait it out.
259
+
260
+ When `lark-cli auth login` returns successfully, tell the user:
261
+ - zh: "✅ 飞书 CLI 已就绪。"
262
+ - en: "✅ Feishu CLI is ready."
263
+
264
+ **Stop — setup is fully complete.**
203
265
 
204
266
  ---
205
267
 
@@ -290,6 +352,105 @@ Tell the user while waiting:
290
352
 
291
353
  ---
292
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
+
293
454
  ## `enable`
294
455
 
295
456
  Call the server API to re-enable the platform (this reads from disk, sets enabled, saves, and hot-reloads):
@@ -337,15 +498,30 @@ Check each item, report ✅ / ❌ with remediation:
337
498
  - Feishu: `app_id`, `app_secret` present and non-empty
338
499
  - WeCom: `bot_id`, `secret` present and non-empty
339
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
340
503
  3. **Feishu credentials** (if enabled) — run the token API call, check `code=0`.
341
504
  4. **Weixin token** (if enabled) — call `GET /api/channels` and check `has_token: true` for the weixin entry.
342
- 5. **WeCom credentials** (if enabled) — search today's log:
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:
343
512
  ```bash
344
513
  grep -iE "wecom adapter loop started|WeCom authentication failed|WeCom WS error response|WecomAdapter" \
345
514
  ~/.clacky/logger/clacky-$(date +%Y-%m-%d).log
346
515
  ```
347
516
  - `WeCom authentication failed` or non-zero errcode → ❌ "WeCom credentials incorrect"
348
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 → ✅
349
525
 
350
526
  ---
351
527
 
@@ -356,7 +532,7 @@ Proactively send a message to a user via an IM channel adapter.
356
532
  ### Parse the request
357
533
 
358
534
  Extract two things from the user's instruction:
359
- - **platform** — one of `weixin`, `feishu`, `wecom`
535
+ - **platform** — one of `weixin`, `feishu`, `wecom`, `discord`, `telegram`
360
536
  - **message** — the text content to send
361
537
 
362
538
  If the platform cannot be inferred, ask the user to clarify.
@@ -396,7 +572,7 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/
396
572
  ### Constraints & notes
397
573
 
398
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.
399
- - **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.
400
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.
401
577
 
402
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
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ # Import lark-cli's official Skills from ~/.agents/skills/lark-* into
7
+ # ~/.clacky/skills/lark-imports/<name>/.
8
+ #
9
+ # Background:
10
+ # lark-cli ships ~24 SKILL.md files (lark-doc, lark-sheets, lark-base, ...)
11
+ # that teach the agent how to use `lark-cli`. They are normally installed
12
+ # under ~/.agents/skills/lark-*, which openclacky's SkillLoader does NOT
13
+ # scan. This importer copies them into ~/.clacky/skills/lark-imports/ so
14
+ # they become discoverable via the standard skill description-matching
15
+ # mechanism.
16
+ #
17
+ # This is intentionally a small, dedicated importer (not a generic external
18
+ # skills tool) — it only handles the lark-cli case for the feishu channel
19
+ # setup flow. Failures are non-fatal: the bot itself remains functional even
20
+ # if Skills cannot be exposed.
21
+ #
22
+ # Usage:
23
+ # importer = Clacky::ChannelSetup::LarkSkillsImporter.new
24
+ # result = importer.run
25
+ # # result => { copied: 24, skipped: 0, errors: [] }
26
+
27
+ module Clacky
28
+ module ChannelSetup
29
+ class LarkSkillsImporter
30
+ DEFAULT_SOURCE_DIR = File.join(Dir.home, '.agents', 'skills')
31
+ DEFAULT_TARGET_DIR = File.join(Dir.home, '.clacky', 'skills', 'lark-imports')
32
+ SKILL_PREFIX = 'lark-'
33
+
34
+ # @param source_dir [String] directory containing lark-cli installed skills
35
+ # @param target_dir [String] destination under ~/.clacky/skills/
36
+ def initialize(source_dir: DEFAULT_SOURCE_DIR, target_dir: DEFAULT_TARGET_DIR)
37
+ @source_dir = Pathname.new(source_dir).expand_path
38
+ @target_dir = Pathname.new(target_dir).expand_path
39
+ end
40
+
41
+ # Run the import. Returns a result hash; never raises on per-skill errors.
42
+ # @return [Hash] { copied: Integer, skipped: Integer, errors: Array<String> }
43
+ def run
44
+ return { copied: 0, skipped: 0, errors: ["source not found: #{@source_dir}"] } unless @source_dir.directory?
45
+
46
+ skill_dirs = discover_lark_skills
47
+ return { copied: 0, skipped: 0, errors: [] } if skill_dirs.empty?
48
+
49
+ FileUtils.mkdir_p(@target_dir)
50
+
51
+ copied = 0
52
+ errors = []
53
+ skill_dirs.each do |src|
54
+ begin
55
+ copy_skill(src)
56
+ copied += 1
57
+ rescue StandardError => e
58
+ errors << "#{src.basename}: #{e.message}"
59
+ end
60
+ end
61
+
62
+ { copied: copied, skipped: 0, errors: errors }
63
+ end
64
+
65
+ # Discover candidate lark-* skill directories under @source_dir.
66
+ # A directory qualifies when it (a) starts with "lark-" and (b) contains a SKILL.md.
67
+ # @return [Array<Pathname>]
68
+ private def discover_lark_skills
69
+ @source_dir.children
70
+ .select { |p| p.directory? && p.basename.to_s.start_with?(SKILL_PREFIX) }
71
+ .select { |p| p.join('SKILL.md').exist? }
72
+ .sort_by { |p| p.basename.to_s }
73
+ end
74
+
75
+ # Copy a single skill directory into @target_dir, replacing any existing copy
76
+ # so re-runs always reflect the latest version.
77
+ # @param src [Pathname]
78
+ private def copy_skill(src)
79
+ dst = @target_dir.join(src.basename.to_s)
80
+ FileUtils.rm_rf(dst) if dst.exist?
81
+ FileUtils.mkdir_p(dst)
82
+ src.children.each { |child| FileUtils.cp_r(child, dst) }
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # CLI entry point — invoked by SKILL.md after the user opts in to lark-cli.
89
+ # Usage:
90
+ # ruby import_lark_skills.rb
91
+ # Prints a one-line summary; exits 0 even when nothing to copy (treat empty
92
+ # source as a soft skip — the script may run before `npx skills add`).
93
+ if $PROGRAM_NAME == __FILE__
94
+ result = Clacky::ChannelSetup::LarkSkillsImporter.new.run
95
+ puts "[lark-import] copied=#{result[:copied]} errors=#{result[:errors].size}"
96
+ result[:errors].each { |e| warn "[lark-import] #{e}" }
97
+ end
@@ -55,7 +55,7 @@ Example (English):
55
55
  > Let's take 30 seconds to personalize your experience — I'll ask just a couple of quick things.
56
56
 
57
57
  Example (Chinese):
58
- > 嗨!我是你的专属小龙虾一号
58
+ > 嗨!我是你的专属员工一号
59
59
  > 只需 30 秒完成个性化设置,我会问你两个简单问题。
60
60
 
61
61
  ### A.3. Ask the user to name the AI (card)