openclacky 1.0.3 → 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 +17 -1
- 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/memory_updater.rb +41 -30
- data/lib/clacky/agent/skill_manager.rb +5 -2
- data/lib/clacky/agent/skill_reflector.rb +10 -1
- data/lib/clacky/agent.rb +4 -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 +56 -2
- 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/http_server.rb +41 -1
- data/lib/clacky/utils/file_processor.rb +71 -0
- data/lib/clacky/version.rb +1 -1
- metadata +31 -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.
|
|
@@ -100,7 +100,7 @@ ruby "SKILL_DIR/feishu_setup.rb"
|
|
|
100
100
|
- The script completed successfully.
|
|
101
101
|
- Config is already written to `~/.clacky/channels.yml`.
|
|
102
102
|
- Tell the user: "✅ Feishu channel configured automatically! The channel is ready."
|
|
103
|
-
- **
|
|
103
|
+
- **Skip Step 2 (manual fallback) and continue to Step 3.**
|
|
104
104
|
|
|
105
105
|
**If exit code is non-0:**
|
|
106
106
|
- Check stdout for the error message.
|
|
@@ -199,7 +199,61 @@ curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/in
|
|
|
199
199
|
-d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}'
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
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.**
|
|
203
257
|
|
|
204
258
|
---
|
|
205
259
|
|
|
@@ -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",
|
|
@@ -57,7 +57,9 @@ module Clacky
|
|
|
57
57
|
def show_assistant_message(content, files:)
|
|
58
58
|
return if content.nil? || content.to_s.strip.empty?
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
# Rewrite local image paths to /api/local-image proxy URLs for browser rendering
|
|
61
|
+
rewritten = Utils::FileProcessor.rewrite_local_image_urls(content.to_s)
|
|
62
|
+
@events << { type: "assistant_message", session_id: @session_id, content: rewritten }
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
def show_tool_call(name, args)
|
|
@@ -397,6 +399,7 @@ module Clacky
|
|
|
397
399
|
when ["POST", "/api/tool/browser"] then api_tool_browser(req, res)
|
|
398
400
|
when ["POST", "/api/upload"] then api_upload_file(req, res)
|
|
399
401
|
when ["POST", "/api/open-file"] then api_open_file(req, res)
|
|
402
|
+
when ["GET", "/api/local-image"] then api_serve_local_image(req, res)
|
|
400
403
|
when ["GET", "/api/version"] then api_get_version(res)
|
|
401
404
|
when ["POST", "/api/version/upgrade"] then api_upgrade_version(req, res)
|
|
402
405
|
when ["POST", "/api/restart"] then api_restart(req, res)
|
|
@@ -1555,6 +1558,43 @@ module Clacky
|
|
|
1555
1558
|
json_response(res, 500, { ok: false, error: e.message })
|
|
1556
1559
|
end
|
|
1557
1560
|
|
|
1561
|
+
# GET /api/local-image?path=file:///path/to/image.png
|
|
1562
|
+
# GET /api/local-image?path=/path/to/image.png
|
|
1563
|
+
#
|
|
1564
|
+
# Serves a local image file with the correct Content-Type.
|
|
1565
|
+
# Used by the Web UI to render local images that would otherwise be blocked
|
|
1566
|
+
# by the browser's security policy (file:// from http:// origin).
|
|
1567
|
+
#
|
|
1568
|
+
def api_serve_local_image(req, res)
|
|
1569
|
+
raw_path = URI.decode_www_form(req.query_string.to_s).to_h["path"].to_s
|
|
1570
|
+
return json_response(res, 400, { error: "path is required" }) if raw_path.empty?
|
|
1571
|
+
|
|
1572
|
+
# Strip file:// prefix if present
|
|
1573
|
+
path = raw_path.sub(%r{\Afile://}, "")
|
|
1574
|
+
path = CGI.unescape(path)
|
|
1575
|
+
path = File.expand_path(path)
|
|
1576
|
+
|
|
1577
|
+
# On WSL the file may be specified as a Windows path (e.g. "C:/Users/…").
|
|
1578
|
+
# Convert it to the Linux-side path so File.exist? works.
|
|
1579
|
+
path = Utils::EnvironmentDetector.win_to_linux_path(path)
|
|
1580
|
+
|
|
1581
|
+
# Security: only serve image files
|
|
1582
|
+
ext = File.extname(path).downcase
|
|
1583
|
+
unless Utils::FileProcessor::LOCAL_IMAGE_EXTENSIONS.include?(ext)
|
|
1584
|
+
return json_response(res, 403, { error: "not an image file" })
|
|
1585
|
+
end
|
|
1586
|
+
|
|
1587
|
+
return json_response(res, 404, { error: "file not found" }) unless File.exist?(path)
|
|
1588
|
+
|
|
1589
|
+
mime = Utils::FileProcessor::MIME_TYPES[ext] || "application/octet-stream"
|
|
1590
|
+
res.status = 200
|
|
1591
|
+
res["Content-Type"] = mime
|
|
1592
|
+
res["Cache-Control"] = "private, max-age=3600"
|
|
1593
|
+
res.body = File.binread(path)
|
|
1594
|
+
rescue => e
|
|
1595
|
+
json_response(res, 500, { error: e.message })
|
|
1596
|
+
end
|
|
1597
|
+
|
|
1558
1598
|
# POST /api/channels/:platform
|
|
1559
1599
|
# Body: { fields... } (platform-specific credential fields)
|
|
1560
1600
|
# Saves credentials and optionally (re)starts the adapter.
|
|
@@ -523,8 +523,79 @@ module Clacky
|
|
|
523
523
|
nil
|
|
524
524
|
end
|
|
525
525
|
|
|
526
|
+
# Image extensions that can be inlined as data URLs in markdown content.
|
|
527
|
+
LOCAL_IMAGE_EXTENSIONS = %w[.png .jpg .jpeg .gif .webp].freeze
|
|
528
|
+
|
|
529
|
+
# Replace local image paths in markdown content with base64 data URLs.
|
|
530
|
+
#
|
|
531
|
+
# Handles both `file:///path/to/img.png` and bare `/path/to/img.png` in
|
|
532
|
+
# markdown image syntax ``.
|
|
533
|
+
#
|
|
534
|
+
# @param content [String] markdown text potentially containing local image references
|
|
535
|
+
# @return [String] content with local images replaced by data URLs
|
|
536
|
+
def self.inline_local_images(content)
|
|
537
|
+
return content if content.nil? || content.empty?
|
|
538
|
+
|
|
539
|
+
content.gsub(%r{(!\[[^\]]*\])\((file://)?(/[^)]+)\)}) do
|
|
540
|
+
prefix = $1
|
|
541
|
+
_scheme = $2
|
|
542
|
+
raw_path = $3
|
|
543
|
+
path = CGI.unescape(raw_path)
|
|
544
|
+
ext = File.extname(path).downcase
|
|
545
|
+
full_match = $&
|
|
546
|
+
|
|
547
|
+
unless LOCAL_IMAGE_EXTENSIONS.include?(ext) && File.exist?(path)
|
|
548
|
+
next full_match
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
begin
|
|
552
|
+
data_url = image_path_to_data_url(path)
|
|
553
|
+
Clacky::Logger.info("file_processor.inline_local_images", path: path, size: File.size(path))
|
|
554
|
+
"#{prefix}(#{data_url})"
|
|
555
|
+
rescue StandardError => e
|
|
556
|
+
Clacky::Logger.warn("file_processor.inline_local_images.failed", path: path, error: e.message)
|
|
557
|
+
full_match
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
526
562
|
private_class_method :parse_zip_listing, :parse_tar_listing, :save_preview, :sanitize_filename,
|
|
527
563
|
:downscale_png_chunky, :downscale_via_cli
|
|
564
|
+
|
|
565
|
+
# -------------------------------------------------------------------------
|
|
566
|
+
# Local image URL rewriting
|
|
567
|
+
# -------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
# Rewrite local image paths in markdown content to use the /api/local-image proxy.
|
|
570
|
+
#
|
|
571
|
+
# Matches two patterns inside ``:
|
|
572
|
+
# 1. file:// URLs → 
|
|
573
|
+
# 2. bare absolute paths → 
|
|
574
|
+
#
|
|
575
|
+
# https:// URLs and non-image files are left untouched.
|
|
576
|
+
#
|
|
577
|
+
# @param content [String, nil] markdown text
|
|
578
|
+
# @return [String, nil] rewritten content (or original if nothing matched)
|
|
579
|
+
def self.rewrite_local_image_urls(content)
|
|
580
|
+
return content if content.nil? || content.empty?
|
|
581
|
+
|
|
582
|
+
content.gsub(/!\[([^\]]*)\]\(((?:file:\/\/)?\/[^)]+)\)/) do |match|
|
|
583
|
+
alt = Regexp.last_match(1)
|
|
584
|
+
href = Regexp.last_match(2)
|
|
585
|
+
|
|
586
|
+
# Extract the filesystem path from the href
|
|
587
|
+
path = href.sub(%r{\Afile://}, "")
|
|
588
|
+
path = CGI.unescape(path)
|
|
589
|
+
|
|
590
|
+
ext = File.extname(path).downcase
|
|
591
|
+
if LOCAL_IMAGE_EXTENSIONS.include?(ext) && File.exist?(path)
|
|
592
|
+
encoded = CGI.escape(href)
|
|
593
|
+
""
|
|
594
|
+
else
|
|
595
|
+
match # return original match unchanged
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
end
|
|
528
599
|
end
|
|
529
600
|
end
|
|
530
601
|
end
|
data/lib/clacky/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -156,6 +156,20 @@ dependencies:
|
|
|
156
156
|
- - ">="
|
|
157
157
|
- !ruby/object:Gem::Version
|
|
158
158
|
version: 0.1.0
|
|
159
|
+
- !ruby/object:Gem::Dependency
|
|
160
|
+
name: logger
|
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - ">="
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '1.4'
|
|
166
|
+
type: :runtime
|
|
167
|
+
prerelease: false
|
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - ">="
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: '1.4'
|
|
159
173
|
- !ruby/object:Gem::Dependency
|
|
160
174
|
name: websocket
|
|
161
175
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -269,6 +283,17 @@ files:
|
|
|
269
283
|
- LICENSE.txt
|
|
270
284
|
- README.md
|
|
271
285
|
- Rakefile
|
|
286
|
+
- benchmark/fixtures/sample_project/Gemfile
|
|
287
|
+
- benchmark/fixtures/sample_project/lib/api_handler.rb
|
|
288
|
+
- benchmark/fixtures/sample_project/lib/order_calculator.rb
|
|
289
|
+
- benchmark/fixtures/sample_project/lib/user_renderer.rb
|
|
290
|
+
- benchmark/fixtures/sample_project/spec/order_calculator_spec.rb
|
|
291
|
+
- benchmark/results/EVALUATION_REPORT.md
|
|
292
|
+
- benchmark/results/baseline_20260511_174424.json
|
|
293
|
+
- benchmark/results/report_20260511_175256.json
|
|
294
|
+
- benchmark/results/report_20260511_175444.json
|
|
295
|
+
- benchmark/results/treatment_20260511_175103.json
|
|
296
|
+
- benchmark/runner.rb
|
|
272
297
|
- bin/clacky
|
|
273
298
|
- bin/clarky
|
|
274
299
|
- bin/openclacky
|
|
@@ -286,6 +311,8 @@ files:
|
|
|
286
311
|
- docs/install-script-simplification.md
|
|
287
312
|
- docs/memory-architecture.md
|
|
288
313
|
- docs/openclacky_cloud_api_reference.md
|
|
314
|
+
- docs/proposals/2026-05-11-system-prompt-alignment.md
|
|
315
|
+
- docs/proposals/2026-05-12-memory-mechanism-optimization.md
|
|
289
316
|
- docs/security-design.md
|
|
290
317
|
- docs/session-management-redesign.md
|
|
291
318
|
- docs/session-skill-invocation.md
|
|
@@ -338,6 +365,7 @@ files:
|
|
|
338
365
|
- lib/clacky/default_skills/browser-setup/SKILL.md
|
|
339
366
|
- lib/clacky/default_skills/channel-setup/SKILL.md
|
|
340
367
|
- lib/clacky/default_skills/channel-setup/feishu_setup.rb
|
|
368
|
+
- lib/clacky/default_skills/channel-setup/import_lark_skills.rb
|
|
341
369
|
- lib/clacky/default_skills/channel-setup/weixin_setup.rb
|
|
342
370
|
- lib/clacky/default_skills/code-explorer/SKILL.md
|
|
343
371
|
- lib/clacky/default_skills/cron-task-creator/SKILL.md
|
|
@@ -348,6 +376,7 @@ files:
|
|
|
348
376
|
- lib/clacky/default_skills/onboard/SKILL.md
|
|
349
377
|
- lib/clacky/default_skills/onboard/scripts/import_external_skills.rb
|
|
350
378
|
- lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb
|
|
379
|
+
- lib/clacky/default_skills/persist-memory/SKILL.md
|
|
351
380
|
- lib/clacky/default_skills/personal-website/SKILL.md
|
|
352
381
|
- lib/clacky/default_skills/personal-website/publish.rb
|
|
353
382
|
- lib/clacky/default_skills/product-help/SKILL.md
|