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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/CONTRIBUTING.md +92 -0
- data/README.md +10 -0
- data/README_CN.md +10 -0
- data/ROADMAP.md +29 -0
- data/docs/billing-system.md +340 -0
- data/docs/mcp-architecture.md +114 -0
- data/docs/mcp.example.json +22 -0
- data/lib/clacky/agent/cost_tracker.rb +37 -0
- data/lib/clacky/agent/llm_caller.rb +0 -1
- data/lib/clacky/agent/session_serializer.rb +2 -11
- data/lib/clacky/agent/skill_manager.rb +73 -26
- data/lib/clacky/agent/system_prompt_builder.rb +0 -5
- data/lib/clacky/agent/time_machine.rb +6 -0
- data/lib/clacky/agent.rb +26 -1
- data/lib/clacky/agent_config.rb +9 -19
- data/lib/clacky/billing/billing_record.rb +67 -0
- data/lib/clacky/billing/billing_store.rb +193 -0
- data/lib/clacky/cli.rb +108 -6
- data/lib/clacky/default_skills/browser-setup/SKILL.md +26 -4
- data/lib/clacky/default_skills/mcp-manager/SKILL.md +343 -0
- data/lib/clacky/idle_compression_timer.rb +4 -2
- data/lib/clacky/mcp/client.rb +204 -0
- data/lib/clacky/mcp/http_transport.rb +155 -0
- data/lib/clacky/mcp/registry.rb +229 -0
- data/lib/clacky/mcp/skill_provider.rb +75 -0
- data/lib/clacky/mcp/stdio_transport.rb +112 -0
- data/lib/clacky/mcp/transport.rb +23 -0
- data/lib/clacky/mcp/virtual_skill.rb +131 -0
- data/lib/clacky/message_history.rb +0 -1
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +2 -35
- data/lib/clacky/server/http_server.rb +519 -15
- data/lib/clacky/server/server_master.rb +8 -14
- data/lib/clacky/server/session_registry.rb +24 -2
- data/lib/clacky/server/web_ui_controller.rb +4 -0
- data/lib/clacky/session_manager.rb +41 -12
- data/lib/clacky/skill.rb +1 -5
- data/lib/clacky/skill_loader.rb +36 -5
- data/lib/clacky/tools/browser.rb +217 -38
- data/lib/clacky/tools/trash_manager.rb +154 -3
- data/lib/clacky/ui2/components/command_suggestions.rb +6 -2
- data/lib/clacky/ui_interface.rb +1 -0
- data/lib/clacky/utils/model_pricing.rb +11 -7
- data/lib/clacky/utils/trash_directory.rb +37 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +2907 -1764
- data/lib/clacky/web/app.js +84 -10
- data/lib/clacky/web/billing.js +275 -0
- data/lib/clacky/web/brand.js +3 -0
- data/lib/clacky/web/i18n.js +242 -24
- data/lib/clacky/web/index.html +351 -134
- data/lib/clacky/web/mcp.js +328 -0
- data/lib/clacky/web/sessions.js +193 -11
- data/lib/clacky/web/settings.js +686 -174
- data/lib/clacky/web/sidebar.js +2 -0
- data/lib/clacky/web/trash.js +323 -60
- data/lib/clacky/web/ws-dispatcher.js +14 -1
- data/lib/clacky.rb +4 -0
- data/scripts/install.ps1 +23 -11
- 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
|
-
|
|
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,
|
|
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
|