openclacky 1.1.6 → 1.2.0

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/CODE_OF_CONDUCT.md +1 -1
  4. data/CONTRIBUTING.md +92 -0
  5. data/README.md +10 -0
  6. data/README_CN.md +10 -0
  7. data/ROADMAP.md +29 -0
  8. data/docs/billing-system.md +340 -0
  9. data/docs/mcp-architecture.md +114 -0
  10. data/docs/mcp.example.json +22 -0
  11. data/lib/clacky/agent/cost_tracker.rb +37 -0
  12. data/lib/clacky/agent/llm_caller.rb +0 -1
  13. data/lib/clacky/agent/session_serializer.rb +2 -11
  14. data/lib/clacky/agent/skill_manager.rb +73 -26
  15. data/lib/clacky/agent/system_prompt_builder.rb +0 -5
  16. data/lib/clacky/agent/time_machine.rb +6 -0
  17. data/lib/clacky/agent.rb +26 -1
  18. data/lib/clacky/agent_config.rb +9 -19
  19. data/lib/clacky/billing/billing_record.rb +67 -0
  20. data/lib/clacky/billing/billing_store.rb +193 -0
  21. data/lib/clacky/cli.rb +108 -6
  22. data/lib/clacky/default_skills/browser-setup/SKILL.md +26 -4
  23. data/lib/clacky/default_skills/mcp-manager/SKILL.md +343 -0
  24. data/lib/clacky/idle_compression_timer.rb +4 -2
  25. data/lib/clacky/mcp/client.rb +204 -0
  26. data/lib/clacky/mcp/http_transport.rb +155 -0
  27. data/lib/clacky/mcp/registry.rb +229 -0
  28. data/lib/clacky/mcp/skill_provider.rb +75 -0
  29. data/lib/clacky/mcp/stdio_transport.rb +112 -0
  30. data/lib/clacky/mcp/transport.rb +23 -0
  31. data/lib/clacky/mcp/virtual_skill.rb +131 -0
  32. data/lib/clacky/message_history.rb +0 -1
  33. data/lib/clacky/server/channel/adapters/weixin/adapter.rb +2 -35
  34. data/lib/clacky/server/http_server.rb +519 -15
  35. data/lib/clacky/server/server_master.rb +8 -14
  36. data/lib/clacky/server/session_registry.rb +24 -2
  37. data/lib/clacky/server/web_ui_controller.rb +4 -0
  38. data/lib/clacky/session_manager.rb +41 -12
  39. data/lib/clacky/skill.rb +1 -5
  40. data/lib/clacky/skill_loader.rb +36 -5
  41. data/lib/clacky/tools/browser.rb +217 -38
  42. data/lib/clacky/tools/trash_manager.rb +154 -3
  43. data/lib/clacky/ui2/components/command_suggestions.rb +6 -2
  44. data/lib/clacky/ui_interface.rb +1 -0
  45. data/lib/clacky/utils/model_pricing.rb +11 -7
  46. data/lib/clacky/utils/trash_directory.rb +37 -6
  47. data/lib/clacky/version.rb +1 -1
  48. data/lib/clacky/web/app.css +2907 -1764
  49. data/lib/clacky/web/app.js +84 -10
  50. data/lib/clacky/web/billing.js +275 -0
  51. data/lib/clacky/web/brand.js +3 -0
  52. data/lib/clacky/web/i18n.js +242 -24
  53. data/lib/clacky/web/index.html +351 -134
  54. data/lib/clacky/web/mcp.js +328 -0
  55. data/lib/clacky/web/sessions.js +193 -11
  56. data/lib/clacky/web/settings.js +686 -174
  57. data/lib/clacky/web/sidebar.js +2 -0
  58. data/lib/clacky/web/trash.js +323 -60
  59. data/lib/clacky/web/ws-dispatcher.js +14 -1
  60. data/lib/clacky.rb +4 -0
  61. data/scripts/install.ps1 +23 -11
  62. metadata +30 -10
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../skill"
4
+
5
+ module Clacky
6
+ module Mcp
7
+ # In-memory Skill that surfaces a configured MCP server in the agent's
8
+ # AVAILABLE MCP SERVERS section. When invoked, it forks a subagent whose
9
+ # only job is to operate this server.
10
+ #
11
+ # The subagent does NOT receive a Ruby-side bridge tool. It calls the
12
+ # server through the local Clacky HTTP API using whichever shell-style
13
+ # tool is already available (curl + the `terminal` tool, etc.). This makes
14
+ # MCP indistinguishable from any other skill at the system-prompt level —
15
+ # there is no second layer for the LLM to misunderstand.
16
+ class VirtualSkill < Clacky::Skill
17
+ attr_reader :mcp_server_name
18
+
19
+ def initialize(server_name:, description:)
20
+ @mcp_server_name = server_name
21
+
22
+ @directory = Pathname.new("/dev/null/mcp/#{server_name}")
23
+ @source_path = @directory
24
+ @brand_skill = false
25
+ @brand_config = nil
26
+ @cached_metadata = nil
27
+ @encrypted = false
28
+ @warnings = []
29
+ @invalid = false
30
+ @invalid_reason = nil
31
+ @frontmatter = {}
32
+
33
+ @name = "mcp:#{server_name}"
34
+ @description = description
35
+ @name_zh = nil
36
+ @description_zh = nil
37
+
38
+ @user_invocable = true
39
+ @disable_model_invocation = false
40
+ @allowed_tools = nil
41
+ @context = nil
42
+ @agent_type = nil
43
+ @argument_hint = nil
44
+ @hooks = nil
45
+ @fork_agent = true
46
+ @model = nil
47
+ @forbidden_tools = nil
48
+ @auto_summarize = true
49
+
50
+ @content = build_content
51
+ end
52
+
53
+ def encrypted?
54
+ false
55
+ end
56
+
57
+ def has_supporting_files?
58
+ false
59
+ end
60
+
61
+ def supporting_files
62
+ []
63
+ end
64
+
65
+ def process_content(shell_output: {}, template_context: {}, script_dir: nil)
66
+ @content
67
+ end
68
+
69
+ def to_h
70
+ super.merge(mcp: true, mcp_server: @mcp_server_name)
71
+ end
72
+
73
+ private def build_content
74
+ <<~MD
75
+ # MCP Server: #{@mcp_server_name}
76
+
77
+ You are a subagent operating the **#{@mcp_server_name}** MCP server through
78
+ the local Clacky HTTP API. Talk to it the same way you would talk to any
79
+ other HTTP service — there is no special MCP tool in your registry.
80
+
81
+ ## Endpoint
82
+
83
+ The Clacky server exposes this MCP server at:
84
+
85
+ http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/mcp/#{@mcp_server_name}
86
+
87
+ Both env vars are already exported in your shell environment.
88
+
89
+ ## Step 1 — Discover available tools
90
+
91
+ Run this once at the start of the task to see the live tool catalog:
92
+
93
+ curl -s "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/mcp/#{@mcp_server_name}/tools"
94
+
95
+ The response shape:
96
+
97
+ { "ok": true, "name": "#{@mcp_server_name}", "tools": [
98
+ { "name": "...", "description": "...", "input_schema": { ... } },
99
+ ...
100
+ ] }
101
+
102
+ Read each tool's `input_schema` to understand its required arguments.
103
+
104
+ ## Step 2 — Invoke a tool
105
+
106
+ curl -s -X POST "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/mcp/#{@mcp_server_name}/call" \\
107
+ -H "Content-Type: application/json" \\
108
+ -d '{"tool":"<tool_name>","arguments":{ ... }}'
109
+
110
+ The response shape:
111
+
112
+ { "ok": true, "result": <raw MCP tools/call result> }
113
+ { "ok": false, "error": "<message>" }
114
+
115
+ The raw `result` typically contains a `content` array with `text` /
116
+ `image` / `resource` parts — extract what you need.
117
+
118
+ ## Workflow
119
+
120
+ 1. Understand the task delegated by the parent agent.
121
+ 2. List tools (Step 1) if you don't already know what's available.
122
+ 3. Pick the right tool(s); call them (Step 2) with valid arguments
123
+ matching each tool's `input_schema`.
124
+ 4. Return a concise summary of what was accomplished and any results
125
+ the parent agent needs. Do not chit-chat — the parent only sees
126
+ your final response.
127
+ MD
128
+ end
129
+ end
130
+ end
131
+ end
@@ -116,7 +116,6 @@ module Clacky
116
116
  last = @messages.last
117
117
  return false unless last[:role] == "assistant" && last[:tool_calls]&.any?
118
118
 
119
- # Check that there is no tool result message after this assistant message
120
119
  last_assistant_idx = @messages.rindex { |m| m == last }
121
120
  @messages[(last_assistant_idx + 1)..].none? { |m| m[:role] == "tool" || m[:tool_results] }
122
121
  end
@@ -300,10 +300,9 @@ module Clacky
300
300
  return { message_id: nil }
301
301
  end
302
302
 
303
- plain = markdown_to_plain(text)
304
- return { message_id: nil } if plain.empty?
303
+ return { message_id: nil } if text.strip.empty?
305
304
 
306
- @send_queue.enqueue(chat_id, plain, ctoken)
305
+ @send_queue.enqueue(chat_id, text, ctoken)
307
306
  { message_id: nil }
308
307
  end
309
308
 
@@ -563,39 +562,7 @@ module Clacky
563
562
  @ctx_mutex.synchronize { @context_tokens.keys.dup }
564
563
  end
565
564
 
566
- # Split text into ≤2000 Unicode character chunks per iLink protocol recommendation.
567
- # Priority: split at \n\n, then \n, then space, then hard cut.
568
- def split_message(text, limit: 2000)
569
- return [text] if text.chars.length <= limit
570
- chunks = []
571
- while text.chars.length > limit
572
- window = text.chars.first(limit).join
573
- # Prefer double-newline boundary
574
- cut = window.rindex("\n\n")
575
- cut = window.rindex("\n") if cut.nil?
576
- cut = window.rindex(" ") if cut.nil?
577
- cut = limit if cut.nil? || cut.zero?
578
- chunks << text.chars.first(cut).join.rstrip
579
- text = text.chars.drop(cut).join.lstrip
580
- end
581
- chunks << text unless text.empty?
582
- chunks
583
- end
584
565
 
585
- # Strip markdown syntax for WeChat (no markdown rendering).
586
- def markdown_to_plain(text)
587
- r = text.dup
588
- r.gsub!(/```[^\n]*\n?([\s\S]*?)```/) { Regexp.last_match(1).strip }
589
- r.gsub!(/!\[[^\]]*\]\([^)]*\)/, "")
590
- r.gsub!(/\[([^\]]+)\]\([^)]*\)/, '\\1')
591
- r.gsub!(/\*\*([^*]+)\*\*/, '\\1')
592
- r.gsub!(/\*([^*]+)\*/, '\\1')
593
- r.gsub!(/__([^_]+)__/, '\\1')
594
- r.gsub!(/_([^_]+)_/, '\\1')
595
- r.gsub!(/^#+\s+/, "")
596
- r.gsub!(/^[-*_]{3,}\s*$/, "")
597
- r.strip
598
- end
599
566
 
600
567
  # ── Typing keepalive ─────────────────────────────────────────────────
601
568
  # sendtyping(status=1) serves dual purpose: maintains typing indicator AND