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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -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 +56 -2
  25. data/lib/clacky/default_skills/channel-setup/import_lark_skills.rb +97 -0
  26. data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
  27. data/lib/clacky/default_skills/persist-memory/SKILL.md +59 -0
  28. data/lib/clacky/providers.rb +48 -6
  29. data/lib/clacky/server/http_server.rb +41 -1
  30. data/lib/clacky/utils/file_processor.rb +71 -0
  31. data/lib/clacky/version.rb +1 -1
  32. 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, 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.
@@ -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
- - **Stop here do not proceed to manual steps.**
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: "✅ Feishu channel configured."
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.
@@ -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). Kimi does not
150
- # distinguish pay-as-you-go vs coding-plan at the base_url level, so
151
- # only two variants are needed. Listing both here lets find_by_base_url
152
- # identify either one as provider "kimi", so downstream capability
153
- # checks, fallback chains, and provider-specific behaviours work
154
- # regardless of which endpoint the user configured.
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
- @events << { type: "assistant_message", session_id: @session_id, content: content }
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 `![alt](src)`.
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 `![alt](url)`:
572
+ # 1. file:// URLs → ![alt](/api/local-image?path=file:///abs/path.png)
573
+ # 2. bare absolute paths → ![alt](/api/local-image?path=/abs/path.png)
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
+ "![#{alt}](/api/local-image?path=#{encoded})"
594
+ else
595
+ match # return original match unchanged
596
+ end
597
+ end
598
+ end
528
599
  end
529
600
  end
530
601
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.0.3"
4
+ VERSION = "1.0.4"
5
5
  end
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.3
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-09 00:00:00.000000000 Z
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