openclacky 1.0.2 → 1.0.4
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 +31 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/docs/proposals/2026-05-11-system-prompt-alignment.md +325 -0
- data/docs/proposals/2026-05-12-memory-mechanism-optimization.md +89 -0
- data/lib/clacky/agent/cost_tracker.rb +8 -2
- data/lib/clacky/agent/llm_caller.rb +218 -0
- data/lib/clacky/agent/memory_updater.rb +41 -30
- data/lib/clacky/agent/message_compressor.rb +15 -4
- data/lib/clacky/agent/message_compressor_helper.rb +41 -2
- data/lib/clacky/agent/skill_manager.rb +5 -2
- data/lib/clacky/agent/skill_reflector.rb +10 -1
- data/lib/clacky/agent/tool_registry.rb +109 -0
- data/lib/clacky/agent.rb +20 -0
- data/lib/clacky/agent_config.rb +17 -0
- data/lib/clacky/cli.rb +65 -0
- data/lib/clacky/client.rb +15 -0
- data/lib/clacky/default_agents/base_prompt.md +20 -20
- data/lib/clacky/default_agents/coding/system_prompt.md +51 -1
- data/lib/clacky/default_skills/channel-setup/SKILL.md +113 -5
- data/lib/clacky/default_skills/channel-setup/import_lark_skills.rb +97 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/clacky/providers.rb +48 -6
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +7 -0
- data/lib/clacky/server/channel/channel_manager.rb +91 -0
- data/lib/clacky/server/discover.rb +77 -0
- data/lib/clacky/server/epipe_safe_io.rb +105 -0
- data/lib/clacky/server/http_server.rb +121 -41
- data/lib/clacky/server/server_master.rb +6 -0
- data/lib/clacky/skill.rb +30 -0
- data/lib/clacky/utils/file_processor.rb +71 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +58 -22
- data/lib/clacky/web/i18n.js +4 -2
- data/lib/clacky/web/sessions.js +29 -17
- metadata +33 -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
|
|
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.
|
|
@@ -4,9 +4,10 @@ description: |
|
|
|
4
4
|
Configure IM platform channels (Feishu, WeCom, Weixin) for openclacky.
|
|
5
5
|
Uses browser automation for navigation; guides the user to paste credentials and perform UI steps.
|
|
6
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
|
-
|
|
9
|
-
|
|
7
|
+
"channel status", "channel enable", "channel disable", "channel reconfigure", "channel doctor",
|
|
8
|
+
"send message to weixin", "send message to feishu", "send message to wecom".
|
|
9
|
+
Subcommands: setup, status, enable <platform>, disable <platform>, reconfigure, doctor, send.
|
|
10
|
+
argument-hint: "setup | status | enable <platform> | disable <platform> | reconfigure | doctor | send <platform> <message>"
|
|
10
11
|
allowed-tools:
|
|
11
12
|
- Bash
|
|
12
13
|
- Read
|
|
@@ -33,6 +34,7 @@ Configure IM platform channels for openclacky.
|
|
|
33
34
|
| `channel disable feishu/wecom/weixin` | disable |
|
|
34
35
|
| `channel reconfigure` | reconfigure |
|
|
35
36
|
| `channel doctor` | doctor |
|
|
37
|
+
| `send <message> to weixin/feishu/wecom` | send |
|
|
36
38
|
|
|
37
39
|
---
|
|
38
40
|
|
|
@@ -98,7 +100,7 @@ ruby "SKILL_DIR/feishu_setup.rb"
|
|
|
98
100
|
- The script completed successfully.
|
|
99
101
|
- Config is already written to `~/.clacky/channels.yml`.
|
|
100
102
|
- Tell the user: "✅ Feishu channel configured automatically! The channel is ready."
|
|
101
|
-
- **
|
|
103
|
+
- **Skip Step 2 (manual fallback) and continue to Step 3.**
|
|
102
104
|
|
|
103
105
|
**If exit code is non-0:**
|
|
104
106
|
- Check stdout for the error message.
|
|
@@ -197,7 +199,61 @@ curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/in
|
|
|
197
199
|
-d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}'
|
|
198
200
|
```
|
|
199
201
|
|
|
200
|
-
Check for `"code":0`. On success:
|
|
202
|
+
Check for `"code":0`. On success: continue to Step 3 (below).
|
|
203
|
+
|
|
204
|
+
##### Phase 9 — done
|
|
205
|
+
|
|
206
|
+
Step 2 ends here. **Continue to Step 3.**
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
#### Step 3 — Optional: install Feishu CLI
|
|
211
|
+
|
|
212
|
+
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.
|
|
213
|
+
|
|
214
|
+
Call `request_user_feedback`:
|
|
215
|
+
|
|
216
|
+
zh:
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"question": "是否要安装「飞书 CLI」?装好之后 AI 可以帮你操作飞书云文档、电子表格、多维表格、知识库、日历、任务等几乎全部飞书能力,不只是聊天,而是能\"做事\"。不装也 OK。",
|
|
220
|
+
"options": ["启用", "跳过"]
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
en:
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"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.",
|
|
228
|
+
"options": ["Enable", "Skip"]
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
If the user picks Skip, stop — setup is complete.
|
|
233
|
+
|
|
234
|
+
If the user picks Enable, run:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
lark-cli --version > /dev/null 2>&1 || npm install -g @larksuite/cli
|
|
238
|
+
echo -n "<APP_SECRET>" | lark-cli config init --app-id <APP_ID> --app-secret-stdin --brand feishu
|
|
239
|
+
npx -y skills add larksuite/cli -y -g
|
|
240
|
+
ruby "SKILL_DIR/import_lark_skills.rb"
|
|
241
|
+
lark-cli auth login --recommend
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The last command blocks up to 10 minutes waiting for browser authorization — make sure the runner's timeout is ≥ 600s.
|
|
245
|
+
|
|
246
|
+
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):
|
|
247
|
+
- zh: "请在浏览器中打开下方链接完成授权:\n<URL>"
|
|
248
|
+
- en: "Open this URL in your browser to authorize:\n<URL>"
|
|
249
|
+
|
|
250
|
+
**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.
|
|
251
|
+
|
|
252
|
+
When `lark-cli auth login` returns successfully, tell the user:
|
|
253
|
+
- zh: "✅ 飞书 CLI 已就绪。"
|
|
254
|
+
- en: "✅ Feishu CLI is ready."
|
|
255
|
+
|
|
256
|
+
**Stop — setup is fully complete.**
|
|
201
257
|
|
|
202
258
|
---
|
|
203
259
|
|
|
@@ -347,6 +403,58 @@ Check each item, report ✅ / ❌ with remediation:
|
|
|
347
403
|
|
|
348
404
|
---
|
|
349
405
|
|
|
406
|
+
## `send`
|
|
407
|
+
|
|
408
|
+
Proactively send a message to a user via an IM channel adapter.
|
|
409
|
+
|
|
410
|
+
### Parse the request
|
|
411
|
+
|
|
412
|
+
Extract two things from the user's instruction:
|
|
413
|
+
- **platform** — one of `weixin`, `feishu`, `wecom`
|
|
414
|
+
- **message** — the text content to send
|
|
415
|
+
|
|
416
|
+
If the platform cannot be inferred, ask the user to clarify.
|
|
417
|
+
|
|
418
|
+
### Step 1 — Resolve target user (optional)
|
|
419
|
+
|
|
420
|
+
If the user specified a `user_id`, use it directly.
|
|
421
|
+
|
|
422
|
+
Otherwise, list known users first:
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
curl -s http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/<platform>/users
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
- If the list is **empty**: tell the user "No known users for `<platform>`. The target user must send at least one message to the bot before proactive messaging is possible." Stop here.
|
|
429
|
+
- If there is **exactly one** user: use it silently.
|
|
430
|
+
- If there are **multiple** users: show the list and ask which one to send to, unless the user already specified one.
|
|
431
|
+
|
|
432
|
+
### Step 2 — Send the message
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/<platform>/send \
|
|
436
|
+
-H "Content-Type: application/json" \
|
|
437
|
+
-d '{"message": "<message>", "user_id": "<user_id>"}'
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Response handling:**
|
|
441
|
+
|
|
442
|
+
| HTTP status | Meaning | Action |
|
|
443
|
+
|---|---|---|
|
|
444
|
+
| `200 { ok: true }` | Delivered | Tell user: "✅ Message sent to `<platform>`." |
|
|
445
|
+
| `400` platform not running | Adapter is stopped | Tell user the platform is not running and suggest `channel enable <platform>`. |
|
|
446
|
+
| `400` no context_token | Token missing | Explain: "The bot has no active session token for this user. Ask the user to send any message to the bot first, then retry." |
|
|
447
|
+
| `503` no known users | Nobody has messaged the bot | Same guidance as empty user list above. |
|
|
448
|
+
| Other error | Unexpected | Show the error message from the response body. |
|
|
449
|
+
|
|
450
|
+
### Constraints & notes
|
|
451
|
+
|
|
452
|
+
- **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.
|
|
454
|
+
- 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
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
350
458
|
## Security
|
|
351
459
|
|
|
352
460
|
- Always mask secrets in output (last 4 chars only).
|
|
@@ -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)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: persist-memory
|
|
3
|
+
description: Persist information to long-term memory at ~/.clacky/memories/. Use when the user asks you to remember/note something, or when reviewing a finished conversation for facts worth keeping. Handles file naming, topic merging, frontmatter, and size limits.
|
|
4
|
+
fork_agent: true
|
|
5
|
+
user-invocable: false
|
|
6
|
+
auto_summarize: true
|
|
7
|
+
forbidden_tools:
|
|
8
|
+
- web_search
|
|
9
|
+
- web_fetch
|
|
10
|
+
- browser
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Persist Memory Subagent
|
|
14
|
+
|
|
15
|
+
You are a **Memory Persistence Subagent** — a pure executor. The caller has already decided that something must be written. Your job is to write it correctly: pick the right file, merge with existing content, respect the size limit.
|
|
16
|
+
|
|
17
|
+
You do NOT decide whether to write. If the task description tells you to persist X, you persist X.
|
|
18
|
+
|
|
19
|
+
## Existing Memory Files
|
|
20
|
+
|
|
21
|
+
The following memory files are pre-loaded for you — **do NOT re-scan the directory** with `terminal` or `file_reader`.
|
|
22
|
+
|
|
23
|
+
<%= memories_meta %>
|
|
24
|
+
|
|
25
|
+
Each file uses YAML frontmatter:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
---
|
|
29
|
+
topic: <topic name>
|
|
30
|
+
description: <one-line description>
|
|
31
|
+
---
|
|
32
|
+
<content in concise Markdown>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Workflow
|
|
36
|
+
|
|
37
|
+
For each item to persist:
|
|
38
|
+
|
|
39
|
+
### Step 1: Pick a target file
|
|
40
|
+
|
|
41
|
+
Scan the list above:
|
|
42
|
+
|
|
43
|
+
- **Matching topic exists** → read it with `file_reader(path: "~/.clacky/memories/<filename>")`, integrate the new info, drop stale parts, then `write` the updated version back.
|
|
44
|
+
- **No match** → create a new file at `~/.clacky/memories/<topic-slug>.md`.
|
|
45
|
+
- Slug: lowercase, hyphen-separated, descriptive (e.g. `deployment-target.md`, `code-style-preferences.md`).
|
|
46
|
+
|
|
47
|
+
### Step 2: Write the file
|
|
48
|
+
|
|
49
|
+
Use the `write` tool. Always include the YAML frontmatter shown above.
|
|
50
|
+
|
|
51
|
+
## Hard constraints (CRITICAL)
|
|
52
|
+
|
|
53
|
+
- Each file MUST stay under **4000 characters of content** (after the frontmatter).
|
|
54
|
+
- If merging would exceed this limit, remove the least important information — do NOT split into multiple files for the same topic.
|
|
55
|
+
- Write concise, factual Markdown — no fluff, no redundant headings.
|
|
56
|
+
- One topic per file. Don't bundle unrelated facts together.
|
|
57
|
+
- Do NOT use `terminal` or `file_reader` to list the memories directory — the list above is authoritative.
|
|
58
|
+
|
|
59
|
+
When done, briefly state what was written (e.g. "Updated deployment-target.md") or `No memory updates needed.` if the task description didn't actually require any writes.
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -146,12 +146,16 @@ module Clacky
|
|
|
146
146
|
"default_model" => "kimi-k2.6",
|
|
147
147
|
"models" => ["kimi-k2.6", "kimi-k2.5"],
|
|
148
148
|
# Moonshot operates two regional endpoints with identical APIs & model
|
|
149
|
-
# lineup — mainland China (.cn) and international (.ai).
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
149
|
+
# lineup — mainland China (.cn) and international (.ai). These are the
|
|
150
|
+
# pay-as-you-go Open Platform endpoints; the subscription-billed
|
|
151
|
+
# Coding Plan lives at api.kimi.com/coding with the unified
|
|
152
|
+
# `kimi-for-coding` model alias and is exposed as a separate
|
|
153
|
+
# top-level "kimi-coding" preset (different domain, distinct billing
|
|
154
|
+
# model, marketed by Moonshot as the standalone Kimi Code product).
|
|
155
|
+
# Listing both PAYG variants here lets find_by_base_url identify
|
|
156
|
+
# either one as provider "kimi", so downstream capability checks,
|
|
157
|
+
# fallback chains, and provider-specific behaviours work regardless
|
|
158
|
+
# of which endpoint the user configured.
|
|
155
159
|
"endpoint_variants" => [
|
|
156
160
|
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.moonshot.cn/v1", "region" => "cn" }.freeze,
|
|
157
161
|
{ "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.moonshot.ai/v1", "region" => "intl" }.freeze
|
|
@@ -161,6 +165,44 @@ module Clacky
|
|
|
161
165
|
"website_url" => "https://platform.moonshot.cn/console/api-keys"
|
|
162
166
|
}.freeze,
|
|
163
167
|
|
|
168
|
+
"kimi-coding" => {
|
|
169
|
+
"name" => "Kimi Code (Coding Plan)",
|
|
170
|
+
# Subscription-billed Kimi Code endpoint — separate product from the
|
|
171
|
+
# PAYG Moonshot Open Platform (api.moonshot.cn/v1 / .ai/v1). Uses the
|
|
172
|
+
# unified `kimi-for-coding` model alias which the Coding Plan backend
|
|
173
|
+
# routes to the appropriate K2 variant (Kimi-k2.6 today; 262K context,
|
|
174
|
+
# 32K max output, supports vision/video/reasoning).
|
|
175
|
+
#
|
|
176
|
+
# Why anthropic-messages: Moonshot exposes the Coding Plan via two
|
|
177
|
+
# URLs on the same domain — an Anthropic-format endpoint at
|
|
178
|
+
# api.kimi.com/coding/ (used by Claude Code via ANTHROPIC_BASE_URL)
|
|
179
|
+
# and an OpenAI-compatible endpoint at api.kimi.com/coding/v1 (used
|
|
180
|
+
# by Roo Code etc.). We route through anthropic-messages so
|
|
181
|
+
# cache_control fields round-trip byte-for-byte (the OpenAI shim is
|
|
182
|
+
# lossy for cache_control semantics — see OpenRouter preset above
|
|
183
|
+
# for the same reason). Verified against the live endpoint: response
|
|
184
|
+
# payload includes cache_creation_input_tokens / cache_read_input_tokens,
|
|
185
|
+
# so the cache layer is real on this backend.
|
|
186
|
+
#
|
|
187
|
+
# User-Agent gate: this endpoint enforces a UA-prefix whitelist
|
|
188
|
+
# limited to first-party coding agents (Kimi CLI, Claude Code, Roo
|
|
189
|
+
# Code, Kilo Code, ...). Requests carrying openclacky's default
|
|
190
|
+
# Faraday UA are rejected with HTTP 403 access_terminated_error.
|
|
191
|
+
# Client#anthropic_connection injects a Claude Code-shaped UA when
|
|
192
|
+
# @provider_id == "kimi-coding" — see the comment in client.rb for
|
|
193
|
+
# the policy rationale.
|
|
194
|
+
#
|
|
195
|
+
# Source: https://www.kimi.com/code/docs/third-party-tools/other-coding-agents.html
|
|
196
|
+
"base_url" => "https://api.kimi.com/coding",
|
|
197
|
+
"api" => "anthropic-messages",
|
|
198
|
+
"default_model" => "kimi-for-coding",
|
|
199
|
+
"models" => ["kimi-for-coding"],
|
|
200
|
+
# K2.6 backend behind the alias is multimodal (image + video input,
|
|
201
|
+
# reasoning). Same vision capability as the PAYG kimi preset.
|
|
202
|
+
"capabilities" => { "vision" => true }.freeze,
|
|
203
|
+
"website_url" => "https://www.kimi.com/code"
|
|
204
|
+
}.freeze,
|
|
205
|
+
|
|
164
206
|
"anthropic" => {
|
|
165
207
|
"name" => "Anthropic (Claude)",
|
|
166
208
|
"base_url" => "https://api.anthropic.com",
|
|
@@ -393,6 +393,13 @@ module Clacky
|
|
|
393
393
|
@ctx_mutex.synchronize { @context_tokens[user_id] }
|
|
394
394
|
end
|
|
395
395
|
|
|
396
|
+
# Return all user IDs for which we have a cached context_token.
|
|
397
|
+
# Used by ChannelManager#known_users so callers can enumerate
|
|
398
|
+
# users reachable for proactive messaging.
|
|
399
|
+
def context_token_user_ids
|
|
400
|
+
@ctx_mutex.synchronize { @context_tokens.keys.dup }
|
|
401
|
+
end
|
|
402
|
+
|
|
396
403
|
# Split text into ≤2000 Unicode character chunks per iLink protocol recommendation.
|
|
397
404
|
# Priority: split at \n\n, then \n, then space, then hard cut.
|
|
398
405
|
def split_message(text, limit: 2000)
|
|
@@ -77,6 +77,74 @@ module Clacky
|
|
|
77
77
|
@mutex.synchronize { @adapters.map(&:platform_id) }
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
# Proactively send a message to a user on the given platform.
|
|
81
|
+
#
|
|
82
|
+
# For Weixin (iLink protocol) a context_token is required for every outbound
|
|
83
|
+
# message. This method looks up the most-recently cached token for user_id.
|
|
84
|
+
# If no token is found the message cannot be delivered and nil is returned.
|
|
85
|
+
#
|
|
86
|
+
# For Feishu and WeCom the chat_id / user_id is sufficient — no token needed.
|
|
87
|
+
#
|
|
88
|
+
# @param platform [Symbol, String] e.g. :weixin, :feishu, :wecom
|
|
89
|
+
# @param user_id [String] IM user identifier
|
|
90
|
+
# @param message [String] plain-text (or markdown) message to send
|
|
91
|
+
# @return [Hash, nil] adapter result hash, or nil on failure
|
|
92
|
+
def send_to_user(platform, user_id, message)
|
|
93
|
+
platform = platform.to_sym
|
|
94
|
+
adapter = @mutex.synchronize { @adapters.find { |a| a.platform_id == platform } }
|
|
95
|
+
|
|
96
|
+
unless adapter
|
|
97
|
+
Clacky::Logger.warn("[ChannelManager] send_to_user: no running adapter for :#{platform}")
|
|
98
|
+
return nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
Clacky::Logger.info("[ChannelManager] send_to_user :#{platform} → #{user_id}")
|
|
102
|
+
adapter.send_text(user_id, message)
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
Clacky::Logger.error("[ChannelManager] send_to_user failed: #{e.message}")
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Return a list of known user IDs for the given platform.
|
|
109
|
+
# Collected from every message that has been processed since the server started.
|
|
110
|
+
# Weixin stores context_tokens keyed by user_id; feishu/wecom track chat_ids
|
|
111
|
+
# via the session binding table in the registry.
|
|
112
|
+
#
|
|
113
|
+
# @param platform [Symbol, String]
|
|
114
|
+
# @return [Array<String>]
|
|
115
|
+
def known_users(platform)
|
|
116
|
+
platform = platform.to_sym
|
|
117
|
+
adapter = @mutex.synchronize { @adapters.find { |a| a.platform_id == platform } }
|
|
118
|
+
return [] unless adapter
|
|
119
|
+
|
|
120
|
+
# Weixin adapter exposes @context_tokens whose keys are user_ids
|
|
121
|
+
if adapter.respond_to?(:context_token_user_ids)
|
|
122
|
+
return adapter.context_token_user_ids
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Fallback: scan session registry for channel_keys matching this platform.
|
|
126
|
+
# Key formats depend on binding_mode:
|
|
127
|
+
# :user → "platform:user:USER_ID"
|
|
128
|
+
# :chat → "platform:chat:CHAT_ID"
|
|
129
|
+
# :chat_user → "platform:chat:CHAT_ID:user:USER_ID"
|
|
130
|
+
#
|
|
131
|
+
# For send_text we need the chat_id (Feishu/WeCom use chat_id as the
|
|
132
|
+
# receive_id for outbound messages), so we extract the chat portion.
|
|
133
|
+
prefix = "#{platform}:"
|
|
134
|
+
ids = []
|
|
135
|
+
@registry.list.each do |summary|
|
|
136
|
+
@registry.with_session(summary[:id]) do |s|
|
|
137
|
+
(s[:channel_keys] || []).each do |key|
|
|
138
|
+
next unless key.start_with?(prefix)
|
|
139
|
+
|
|
140
|
+
remainder = key.sub(prefix, "") # e.g. "chat:OC_ID:user:OU_ID" or "user:UID" or "chat:CID"
|
|
141
|
+
ids << extract_chat_id(remainder)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
ids.compact.uniq
|
|
146
|
+
end
|
|
147
|
+
|
|
80
148
|
# Hot-reload a single platform adapter with updated config.
|
|
81
149
|
# Stops the existing adapter (if running), then starts a new one if enabled.
|
|
82
150
|
# @param platform [Symbol]
|
|
@@ -367,6 +435,29 @@ module Clacky
|
|
|
367
435
|
end
|
|
368
436
|
end
|
|
369
437
|
|
|
438
|
+
# Extract the chat_id from the remainder of a channel_key (after removing "platform:" prefix).
|
|
439
|
+
#
|
|
440
|
+
# Possible formats:
|
|
441
|
+
# "chat:CHAT_ID:user:USER_ID" → CHAT_ID (chat_user mode)
|
|
442
|
+
# "chat:CHAT_ID" → CHAT_ID (chat mode)
|
|
443
|
+
# "user:USER_ID" → USER_ID (user mode — use user_id as fallback)
|
|
444
|
+
#
|
|
445
|
+
# For Feishu/WeCom send_text, the chat_id is what's needed as receive_id.
|
|
446
|
+
private def extract_chat_id(remainder)
|
|
447
|
+
if remainder.start_with?("chat:")
|
|
448
|
+
# "chat:CHAT_ID:user:USER_ID" or "chat:CHAT_ID"
|
|
449
|
+
after_chat = remainder.sub("chat:", "")
|
|
450
|
+
# If there's a ":user:" segment, strip it and everything after
|
|
451
|
+
idx = after_chat.index(":user:")
|
|
452
|
+
idx ? after_chat[0...idx] : after_chat
|
|
453
|
+
elsif remainder.start_with?("user:")
|
|
454
|
+
# user-only mode: no chat_id available, use user_id
|
|
455
|
+
remainder.sub("user:", "")
|
|
456
|
+
else
|
|
457
|
+
remainder
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
370
461
|
def safe_stop_adapter(adapter)
|
|
371
462
|
adapter.stop
|
|
372
463
|
rescue StandardError => e
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
module Server
|
|
8
|
+
# Discover locally-running Clacky server(s) by scanning PID files
|
|
9
|
+
# written by Master at /tmp/clacky-master-<port>.pid.
|
|
10
|
+
#
|
|
11
|
+
# Used by the CLI (bare `clacky agent` mode) to auto-detect a sibling
|
|
12
|
+
# server process, so skills that call back into the server (channels,
|
|
13
|
+
# browser, scheduler, etc.) can work without the user manually setting
|
|
14
|
+
# CLACKY_SERVER_HOST / CLACKY_SERVER_PORT.
|
|
15
|
+
#
|
|
16
|
+
# Fast and side-effect free: only reads files and sends signal 0.
|
|
17
|
+
# Does NOT probe the TCP port (avoids false positives from stale files
|
|
18
|
+
# but also avoids noisy connection attempts).
|
|
19
|
+
module Discover
|
|
20
|
+
PID_FILE_GLOB = File.join(Dir.tmpdir, "clacky-master-*.pid").freeze
|
|
21
|
+
PID_FILE_REGEX = /clacky-master-(\d+)\.pid\z/.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# Find the first live Clacky server on this machine.
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash, nil] { host: "127.0.0.1", port: Integer, pid: Integer } or nil
|
|
28
|
+
def find_local
|
|
29
|
+
find_all_local.first
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Find all live Clacky servers on this machine.
|
|
33
|
+
#
|
|
34
|
+
# A PID file is considered "live" when:
|
|
35
|
+
# 1. The filename matches clacky-master-<port>.pid
|
|
36
|
+
# 2. Its contents parse as a positive integer
|
|
37
|
+
# 3. Process.kill(0, pid) confirms the PID is alive
|
|
38
|
+
#
|
|
39
|
+
# Stale PID files (process gone) are silently ignored. We do NOT
|
|
40
|
+
# delete them here — that's the owning server's responsibility.
|
|
41
|
+
#
|
|
42
|
+
# @return [Array<Hash>] sorted by port ascending
|
|
43
|
+
def find_all_local
|
|
44
|
+
Dir.glob(PID_FILE_GLOB).filter_map do |path|
|
|
45
|
+
m = path.match(PID_FILE_REGEX)
|
|
46
|
+
next nil unless m
|
|
47
|
+
|
|
48
|
+
port = m[1].to_i
|
|
49
|
+
next nil if port <= 0
|
|
50
|
+
|
|
51
|
+
pid_str = File.read(path).strip rescue nil
|
|
52
|
+
next nil if pid_str.nil? || pid_str.empty?
|
|
53
|
+
|
|
54
|
+
pid = pid_str.to_i
|
|
55
|
+
next nil if pid <= 0
|
|
56
|
+
|
|
57
|
+
next nil unless process_alive?(pid)
|
|
58
|
+
|
|
59
|
+
{ host: "127.0.0.1", port: port, pid: pid }
|
|
60
|
+
end.sort_by { |e| e[:port] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param pid [Integer]
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def process_alive?(pid)
|
|
66
|
+
Process.kill(0, pid)
|
|
67
|
+
true
|
|
68
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
69
|
+
# ESRCH — no such process; EPERM — process exists but owned by someone else
|
|
70
|
+
# (still technically alive, but we can't safely assume it's "our" server)
|
|
71
|
+
false
|
|
72
|
+
rescue StandardError
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|