openclacky 1.5.3 → 1.5.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +13 -0
  3. data/CHANGELOG.md +26 -0
  4. data/Dockerfile +21 -1
  5. data/README.md +26 -7
  6. data/README_CN.md +26 -7
  7. data/README_JA.md +26 -7
  8. data/lib/clacky/agent/message_compressor.rb +4 -29
  9. data/lib/clacky/agent/message_compressor_helper.rb +13 -15
  10. data/lib/clacky/agent/session_serializer.rb +6 -0
  11. data/lib/clacky/agent.rb +13 -1
  12. data/lib/clacky/agent_config.rb +6 -3
  13. data/lib/clacky/brand_config.rb +32 -12
  14. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +15 -0
  15. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +1 -5
  16. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1 -1
  17. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +5 -1
  18. data/lib/clacky/default_parsers/xlsx_parser.py +66 -0
  19. data/lib/clacky/extension/api_extension.rb +3 -2
  20. data/lib/clacky/message_format/open_ai.rb +26 -3
  21. data/lib/clacky/providers.rb +16 -8
  22. data/lib/clacky/server/http_server.rb +341 -24
  23. data/lib/clacky/server/project_manager.rb +150 -0
  24. data/lib/clacky/server/server_master.rb +7 -0
  25. data/lib/clacky/server/session_registry.rb +14 -1
  26. data/lib/clacky/session_manager.rb +5 -4
  27. data/lib/clacky/tools/file_reader.rb +2 -1
  28. data/lib/clacky/utils/model_pricing.rb +69 -18
  29. data/lib/clacky/utils/parser_manager.rb +115 -7
  30. data/lib/clacky/version.rb +1 -1
  31. data/lib/clacky/web/app.css +903 -135
  32. data/lib/clacky/web/app.js +11 -2
  33. data/lib/clacky/web/components/notify.js +139 -22
  34. data/lib/clacky/web/core/aside.js +21 -0
  35. data/lib/clacky/web/features/brand/store.js +3 -1
  36. data/lib/clacky/web/features/brand/view.js +29 -5
  37. data/lib/clacky/web/features/new-session/view.js +2 -2
  38. data/lib/clacky/web/features/trash/store.js +23 -9
  39. data/lib/clacky/web/features/trash/view.js +51 -0
  40. data/lib/clacky/web/i18n.js +95 -4
  41. data/lib/clacky/web/index.html +69 -15
  42. data/lib/clacky/web/projects.js +1143 -0
  43. data/lib/clacky/web/sessions.js +318 -537
  44. data/lib/clacky/web/settings.js +138 -5
  45. data/lib/clacky/web/ws-dispatcher.js +59 -1
  46. data/scripts/build/src/install_system_deps.sh.cc +11 -3
  47. data/scripts/install_system_deps.sh +11 -3
  48. metadata +5 -2
  49. data/lib/clacky/default_parsers/xlsx_parser.rb +0 -121
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04d2d81c4aaa380d4600066802c78e1137bdad1cb16c3e5e47e32e69ede15da6
4
- data.tar.gz: 9a332e0305e96bae97799ce707ef492b24735e33ef8da70a390c06a10ac11ab4
3
+ metadata.gz: 10e414552c9ae0e3bb7a0b547f2e4da0709c83e0a0925f3b708820848e1a4cf3
4
+ data.tar.gz: 6caa29ccd3048e95912500be99000ae7ca883c9a5ad473ebc1bb2f243aef5cc3
5
5
  SHA512:
6
- metadata.gz: 435b6c1bd6aac4c2916cf172e5d46dece9a5707d1b18119d4744d29293c1f7b8fb112709718d24b8b9b3a871bfa228294a282525d9b6f7ed14550b89eaaa2793
7
- data.tar.gz: 90599042f3d933db9b7681c33d539937585cd85542d635696ea159056aa47dbe34f86971bd1db6541c4dee28ee73c66e8f4648d91d8f3b1897b179ce9a2b00fc
6
+ metadata.gz: 26f220973c6821a411362df35ca281ef53ae58e6ab36d26651db393b31713340ca9bcf131f69bf4e6aa391eec3f4f21fc7d0d12cb4bbbdc368b6fe8068fa2dfa
7
+ data.tar.gz: 84faf59b60486d29a9168f847c97d7fd5670dac293f967bd713c08049612adab7f947fa38af3a7e89c4a5acc2cc8c460e9b8ebbf53c189c19a52bf7e89a44388
data/.dockerignore ADDED
@@ -0,0 +1,13 @@
1
+ .git
2
+ .github
3
+ .clacky
4
+ vendor
5
+ spec
6
+ docs
7
+ tmp
8
+ coverage
9
+ pkg
10
+ *.gem
11
+ .rspec_status
12
+ AGENTS.md
13
+ CLAUDE.md
data/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
 
7
+ ## [1.5.4] - 2026-08-01
8
+
9
+ ### Added
10
+ - Project management: create, manage, and switch between projects
11
+ - Claude Sonnet 5 and Opus 5 models with pricing
12
+ - Desktop notifications for background task completion
13
+ - Configurable platform source and branding restoration
14
+ - Hidden flag on create_session API (#403)
15
+ - MiniMax preset refreshed with Anthropic endpoints, M3 thinking, and current lineup (#402)
16
+
17
+ ### Improved
18
+ - File upload size limit increased from 32 MB to 300 MB
19
+ - Dir picker UX: breadcrumb overflow scroll, row spacing, and padding
20
+ - Session-sidebar collapse button appearance matches opener button
21
+
22
+ ### Fixed
23
+ - XLSX parser switched to Python/openpyxl with 60s timeout kill for reliable parsing (C-5656)
24
+ - Panel agents map missing general profile on first load
25
+ - Stale result on first load due to container extension script execution order
26
+ - Page now reloads after deleting local extension in ext-studio
27
+ - Accent color used for extensions filter tab active underline
28
+ - Missing pricing for Claude Sonnet 5 and Opus 5
29
+ - Docker image builds from source and publishes to GHCR on version tags
30
+ - Session list page size reduced from 20 to 15 for better performance
31
+ - MiniMax-M2.5 restored, default base_url reverted to mainland
32
+
7
33
  ## [1.5.3] - 2026-07-28
8
34
 
9
35
  ### Added
data/Dockerfile CHANGED
@@ -1,14 +1,34 @@
1
1
  FROM ruby:3.4.4-slim AS builder
2
2
 
3
+ ARG VERSION
4
+
3
5
  RUN apt-get update && apt-get install -y \
4
6
  build-essential \
5
7
  curl \
8
+ git \
6
9
  && rm -rf /var/lib/apt/lists/*
7
10
 
8
- RUN gem install openclacky --no-document
11
+ # Build from repository source so the image matches this checkout (not a published gem).
12
+ # gemspec uses `git ls-files`; seed a temporary git index because .git is dockerignored.
13
+ COPY . /src
14
+ WORKDIR /src
15
+ RUN git init \
16
+ && git config user.email "docker@local" \
17
+ && git config user.name "docker" \
18
+ && git add -A \
19
+ && git commit -m "docker build" \
20
+ && gem build openclacky.gemspec \
21
+ && gem install ./openclacky-*.gem --no-document \
22
+ && ruby -e 'require "clacky"; abort "bad version" unless Clacky::VERSION'
9
23
 
10
24
  FROM ruby:3.4.4-slim
11
25
 
26
+ ARG VERSION
27
+ LABEL org.opencontainers.image.title="openclacky" \
28
+ org.opencontainers.image.description="OpenClacky AI agent CLI and Web UI" \
29
+ org.opencontainers.image.source="https://github.com/clacky-ai/openclacky" \
30
+ org.opencontainers.image.version="${VERSION}"
31
+
12
32
  RUN apt-get update && apt-get install -y \
13
33
  git \
14
34
  curl \
data/README.md CHANGED
@@ -110,18 +110,24 @@ see more: https://www.openclacky.com/docs/installation
110
110
 
111
111
  ### Docker
112
112
 
113
- Build:
113
+ #### Pre-built image (GHCR)
114
+
115
+ Images are published to GitHub Container Registry on version tags (`v*`).
114
116
 
115
117
  ```bash
116
- git clone https://github.com/clacky-ai/openclacky.git
117
- cd openclacky
118
- docker build -t openclacky .
118
+ # Replace <owner> with the repo owner (e.g. clacky-ai or your fork)
119
+ docker pull ghcr.io/<owner>/openclacky:latest
120
+ # or pin a release:
121
+ # docker pull ghcr.io/<owner>/openclacky:1.5.3
119
122
  ```
120
123
 
121
124
  **Linux:**
122
125
 
123
126
  ```bash
124
- docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
127
+ docker run -d --name openclacky --network=host \
128
+ -e CLACKY_ACCESS_KEY="" \
129
+ -v openclacky-data:/root/.clacky \
130
+ ghcr.io/<owner>/openclacky:latest
125
131
  ```
126
132
 
127
133
  `--network=host` is required so the agent inside the container can reach Chrome's remote debugging port running on the host.
@@ -129,18 +135,31 @@ docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
129
135
  **macOS / Windows:**
130
136
 
131
137
  ```bash
132
- docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
138
+ docker run -d --name openclacky -p 7070:7070 \
139
+ -e CLACKY_ACCESS_KEY="" \
140
+ -v openclacky-data:/root/.clacky \
141
+ ghcr.io/<owner>/openclacky:latest
133
142
  ```
134
143
 
135
144
  > **Note:** On macOS/Windows, `--network=host` is not supported — browser automation may be limited.
136
145
 
137
146
  Open **http://localhost:7070** after starting.
138
147
 
148
+ #### Build from source
149
+
150
+ ```bash
151
+ git clone https://github.com/clacky-ai/openclacky.git
152
+ cd openclacky
153
+ # optional: set the OCI image version label
154
+ docker build --build-arg VERSION=1.5.3 -t openclacky .
155
+ docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
156
+ ```
157
+
139
158
  Environment variables:
140
159
 
141
160
  | Variable | Description |
142
161
  |---|---|
143
- | `CLACKY_ACCESS_KEY` | Protect the Web UI with an access key (empty = public mode) |
162
+ | `CLACKY_ACCESS_KEY` | Protect the Web UI with an access key (empty = public mode; env must be present when binding `0.0.0.0`) |
144
163
 
145
164
 
146
165
  ## Quick Start
data/README_CN.md CHANGED
@@ -106,18 +106,24 @@ gem install openclacky
106
106
 
107
107
  ### Docker
108
108
 
109
- 构建:
109
+ #### 预构建镜像(GHCR)
110
+
111
+ 镜像在版本 tag(`v*`)时自动发布到 GitHub Container Registry。
110
112
 
111
113
  ```bash
112
- git clone https://github.com/clacky-ai/openclacky.git
113
- cd openclacky
114
- docker build -t openclacky .
114
+ # <owner> 替换为仓库所有者(如 clacky-ai 或你的 fork)
115
+ docker pull ghcr.io/<owner>/openclacky:latest
116
+ # 或固定某个版本:
117
+ # docker pull ghcr.io/<owner>/openclacky:1.5.3
115
118
  ```
116
119
 
117
120
  **Linux:**
118
121
 
119
122
  ```bash
120
- docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
123
+ docker run -d --name openclacky --network=host \
124
+ -e CLACKY_ACCESS_KEY="" \
125
+ -v openclacky-data:/root/.clacky \
126
+ ghcr.io/<owner>/openclacky:latest
121
127
  ```
122
128
 
123
129
  `--network=host` 使容器与宿主机共享网络栈,Agent 可直接访问宿主机上运行的 Chrome 远程调试端口。
@@ -125,18 +131,31 @@ docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
125
131
  **macOS / Windows:**
126
132
 
127
133
  ```bash
128
- docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
134
+ docker run -d --name openclacky -p 7070:7070 \
135
+ -e CLACKY_ACCESS_KEY="" \
136
+ -v openclacky-data:/root/.clacky \
137
+ ghcr.io/<owner>/openclacky:latest
129
138
  ```
130
139
 
131
140
  > **注意:** macOS/Windows 不支持 `--network=host`,浏览器自动化功能可能受限。
132
141
 
133
142
  启动后访问 **http://localhost:7070**。
134
143
 
144
+ #### 从源码构建
145
+
146
+ ```bash
147
+ git clone https://github.com/clacky-ai/openclacky.git
148
+ cd openclacky
149
+ # 可选:设置 OCI 镜像版本标签
150
+ docker build --build-arg VERSION=1.5.3 -t openclacky .
151
+ docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
152
+ ```
153
+
135
154
  环境变量:
136
155
 
137
156
  | 变量 | 说明 |
138
157
  |---|---|
139
- | `CLACKY_ACCESS_KEY` | 设置访问密钥保护 Web UI(留空 = 公开模式) |
158
+ | `CLACKY_ACCESS_KEY` | 设置访问密钥保护 Web UI(留空 = 公开模式;绑定 `0.0.0.0` 时必须设置该环境变量) |
140
159
 
141
160
  ## 快速开始
142
161
 
data/README_JA.md CHANGED
@@ -110,18 +110,24 @@ gem install openclacky
110
110
 
111
111
  ### Docker
112
112
 
113
- ビルド:
113
+ #### ビルド済みイメージ(GHCR)
114
+
115
+ バージョンタグ(`v*`)が付くと GitHub Container Registry にイメージが自動公開されます。
114
116
 
115
117
  ```bash
116
- git clone https://github.com/clacky-ai/openclacky.git
117
- cd openclacky
118
- docker build -t openclacky .
118
+ # <owner> をリポジトリオーナーに置き換えてください(例: clacky-ai またはあなたのフォーク)
119
+ docker pull ghcr.io/<owner>/openclacky:latest
120
+ # またはバージョンを固定する場合:
121
+ # docker pull ghcr.io/<owner>/openclacky:1.5.3
119
122
  ```
120
123
 
121
124
  **Linux:**
122
125
 
123
126
  ```bash
124
- docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
127
+ docker run -d --name openclacky --network=host \
128
+ -e CLACKY_ACCESS_KEY="" \
129
+ -v openclacky-data:/root/.clacky \
130
+ ghcr.io/<owner>/openclacky:latest
125
131
  ```
126
132
 
127
133
  `--network=host` は、コンテナ内のエージェントがホスト上で動作する Chrome のリモートデバッグポートに到達するために必要です。
@@ -129,18 +135,31 @@ docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
129
135
  **macOS / Windows:**
130
136
 
131
137
  ```bash
132
- docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
138
+ docker run -d --name openclacky -p 7070:7070 \
139
+ -e CLACKY_ACCESS_KEY="" \
140
+ -v openclacky-data:/root/.clacky \
141
+ ghcr.io/<owner>/openclacky:latest
133
142
  ```
134
143
 
135
144
  > **注意:** macOS/Windows では `--network=host` がサポートされていないため、ブラウザの自動化が制限される場合があります。
136
145
 
137
146
  起動後、**http://localhost:7070** を開いてください。
138
147
 
148
+ #### ソースからビルド
149
+
150
+ ```bash
151
+ git clone https://github.com/clacky-ai/openclacky.git
152
+ cd openclacky
153
+ # オプション: OCI イメージバージョンラベルを設定
154
+ docker build --build-arg VERSION=1.5.3 -t openclacky .
155
+ docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
156
+ ```
157
+
139
158
  環境変数:
140
159
 
141
160
  | 変数 | 説明 |
142
161
  |---|---|
143
- | `CLACKY_ACCESS_KEY` | アクセスキーで Web UI を保護します(空の場合はパブリックモード) |
162
+ | `CLACKY_ACCESS_KEY` | アクセスキーで Web UI を保護します(空の場合はパブリックモード;`0.0.0.0` にバインドする場合は必須) |
144
163
 
145
164
 
146
165
  ## クイックスタート
@@ -39,14 +39,10 @@ module Clacky
39
39
 
40
40
  REQUIRED RESPONSE FORMAT:
41
41
  First output a <topics> line listing 3-6 key topic phrases (comma-separated, concise).
42
- Then output a <continues_previous> line: "true" if this conversation is a direct
43
- continuation of the SAME task/topic as the PREVIOUS chunk shown below, "false" if it
44
- has moved on to a different task or topic. If there is no previous chunk, output "false".
45
42
  Then output the full summary wrapped in <summary> tags.
46
43
 
47
44
  Example format:
48
45
  <topics>Rails setup, database config, deploy pipeline, Tailwind CSS</topics>
49
- <continues_previous>false</continues_previous>
50
46
  <summary>
51
47
  ...full summary text...
52
48
  </summary>
@@ -58,8 +54,7 @@ module Clacky
58
54
  - Errors encountered and fixes applied
59
55
  - Current work status and pending tasks
60
56
 
61
- Begin your response NOW. Remember: PURE TEXT only, starting with <topics> then
62
- <continues_previous> then <summary>.
57
+ Begin your response NOW. Remember: PURE TEXT only, starting with <topics> then <summary>.
63
58
  PROMPT
64
59
 
65
60
  def initialize(client, model: nil)
@@ -77,9 +72,6 @@ module Clacky
77
72
  #
78
73
  # @param messages [Array<Hash>] Original conversation messages
79
74
  # @param recent_messages [Array<Hash>] Recent messages to keep uncompressed (optional)
80
- # @param previous_topics [String, nil] Topics of the most recent chunk on disk,
81
- # shown to the LLM so it can decide whether the current conversation is a
82
- # continuation (drives the <continues_previous> output for chunk merging).
83
75
  # @return [Hash] Compression instruction message to insert, or nil if nothing to compress
84
76
  def build_compression_message(messages, recent_messages: [], previous_topics: nil)
85
77
  # Get messages to compress (exclude system message and recent messages)
@@ -88,14 +80,9 @@ module Clacky
88
80
  # If nothing to compress, return nil
89
81
  return nil if messages_to_compress.empty?
90
82
 
91
- content = COMPRESSION_PROMPT
92
- if previous_topics && !previous_topics.strip.empty?
93
- content = "#{COMPRESSION_PROMPT}\n\nPREVIOUS CHUNK TOPICS (for <continues_previous> judgement): #{previous_topics}"
94
- end
95
-
96
83
  {
97
84
  role: "user",
98
- content: content,
85
+ content: COMPRESSION_PROMPT,
99
86
  system_injected: true
100
87
  }
101
88
  end
@@ -154,15 +141,6 @@ module Clacky
154
141
  m ? m[1].strip : nil
155
142
  end
156
143
 
157
- # Parse the <continues_previous> tag. Returns true only when the LLM
158
- # explicitly says "true"; missing tag or any other value → false.
159
- # This conservative default ensures we never merge unless the model is sure.
160
- def parse_continues_previous(content)
161
- return false if content.nil? || content.to_s.empty?
162
- m = content.to_s.match(/<continues_previous>(.*?)<\/continues_previous>/m)
163
- m ? m[1].strip.downcase == "true" : false
164
- end
165
-
166
144
  def parse_compressed_result(result, chunk_path: nil, topics: nil, previous_chunks: [])
167
145
  # Return the compressed result as a single user message (role: "user").
168
146
  #
@@ -192,11 +170,8 @@ module Clacky
192
170
  if content.empty?
193
171
  []
194
172
  else
195
- # Strip out the <topics> and <continues_previous> blocks they're
196
- # metadata for chunk handling, not for AI context.
197
- content_without_topics = content.gsub(/<topics>.*?<\/topics>\n*/m, "")
198
- .gsub(/<continues_previous>.*?<\/continues_previous>\n*/m, "")
199
- .strip
173
+ # Strip out the <topics> block it's metadata for chunk handling, not for AI context.
174
+ content_without_topics = content.gsub(/<topics>.*?<\/topics>\n*/m, "").strip
200
175
 
201
176
  # Build previous chunks index section — links to older chunk files so the AI
202
177
  # can find earlier conversations without keeping all prior compressed_summary
@@ -200,21 +200,19 @@ module Clacky
200
200
  recent_messages = get_recent_messages_with_tool_pairs(all_messages, target_recent_count)
201
201
  recent_messages = [] if recent_messages.nil?
202
202
 
203
- # Surface the most recent chunk's topics so the compression LLM can judge
204
- # whether this conversation continues the same task (drives chunk merging).
205
- previous_topics = nil
206
- if @session_id && @created_at
207
- latest = session_manager.chunks_for_current(@session_id, @created_at).last
208
- previous_topics = latest && latest[:topics]
209
- end
210
-
211
203
  # Build compression instruction message (to be inserted into conversation)
212
204
  compression_message = @message_compressor.build_compression_message(
213
- all_messages, recent_messages: recent_messages, previous_topics: previous_topics
205
+ all_messages, recent_messages: recent_messages
214
206
  )
215
207
 
216
208
  return nil if compression_message.nil?
217
209
 
210
+ # Active (non-forced) compression means we've accumulated enough
211
+ # conversation to hit the normal thresholds — always create a new chunk.
212
+ # Idle / context-overflow compressions (force: true) always merge into
213
+ # the previous chunk to avoid fragmenting small batches.
214
+ force_new_chunk = !force
215
+
218
216
  # Return compression context for agent to handle
219
217
  {
220
218
  compression_message: compression_message,
@@ -222,7 +220,8 @@ module Clacky
222
220
  pulled_back_messages: pulled_back_messages,
223
221
  original_token_count: total_tokens,
224
222
  original_message_count: @history.size,
225
- compression_level: @compression_level
223
+ compression_level: @compression_level,
224
+ force_new_chunk: force_new_chunk
226
225
  }
227
226
  end
228
227
 
@@ -265,12 +264,11 @@ module Clacky
265
264
  topics = @message_compressor.parse_topics(compressed_content)
266
265
 
267
266
  # Decide whether to MERGE into the previous chunk or create a NEW one.
268
- # The LLM judges (via <continues_previous>) whether this conversation is a
269
- # direct continuation of the previous chunk's task. Merging avoids tiny
270
- # fragmented chunks (e.g. a long task compressed mid-flight into 2-message
271
- # chunks) that pollute the topics index and degrade recall.
267
+ # Active compression (force_new_chunk: true) always creates a new chunk.
268
+ # Idle / overflow compression always merges into the previous chunk if
269
+ # one exists, avoiding tiny fragmented chunks that pollute the topics index.
272
270
  latest_chunk = existing_chunks.last
273
- continues = latest_chunk && @message_compressor.parse_continues_previous(compressed_content)
271
+ continues = latest_chunk && !compression_context[:force_new_chunk]
274
272
 
275
273
  if continues
276
274
  chunk_path = merge_into_previous_chunk(
@@ -11,6 +11,7 @@ module Clacky
11
11
  @session_id = session_data[:session_id]
12
12
  @name = session_data[:name] || ""
13
13
  @pinned = session_data[:pinned] || false
14
+ @hidden = session_data[:hidden] || false
14
15
  @history = MessageHistory.new(session_data[:messages] || [])
15
16
  @todos = session_data[:todos] || [] # Restore todos from session
16
17
  @iterations = session_data.dig(:stats, :total_iterations) || 0
@@ -29,6 +30,9 @@ module Clacky
29
30
  # Restore channel info for IM platform sessions
30
31
  @channel_info = session_data[:channel_info]
31
32
 
33
+ # Restore project association (nil = no project)
34
+ @project_id = session_data[:project_id]
35
+
32
36
  # Restore cache statistics if available
33
37
  @cache_stats = session_data.dig(:stats, :cache_stats) || {
34
38
  cache_creation_input_tokens: 0,
@@ -200,6 +204,7 @@ module Clacky
200
204
  session_id: @session_id,
201
205
  name: @name,
202
206
  pinned: @pinned,
207
+ hidden: @hidden,
203
208
  created_at: @created_at,
204
209
  updated_at: (updated_at || (preserve_updated_at && @persisted_updated_at) || Time.now.iso8601).then { |v| v.is_a?(String) ? v : v.iso8601 },
205
210
  working_dir: @working_dir,
@@ -234,6 +239,7 @@ module Clacky
234
239
  sub_model: @config.session_model_overlay_name
235
240
  },
236
241
  channel_info: @channel_info,
242
+ project_id: @project_id,
237
243
  stats: stats_data,
238
244
  messages: @history.to_a
239
245
  }
data/lib/clacky/agent.rb CHANGED
@@ -49,7 +49,9 @@ module Clacky
49
49
  :latest_latency, # Hash of latency metrics from the most recent LLM call (see Client#send_messages_with_tools)
50
50
  :reasoning_effort
51
51
  attr_accessor :pinned
52
+ attr_accessor :hidden
52
53
  attr_accessor :channel_info
54
+ attr_accessor :project_id
53
55
 
54
56
  REASONING_EFFORTS = %w[low medium high xhigh].freeze
55
57
 
@@ -82,6 +84,7 @@ module Clacky
82
84
  @session_id = session_id
83
85
  @name = ""
84
86
  @pinned = false
87
+ @hidden = false
85
88
  @history = MessageHistory.new
86
89
  @todos = [] # Store todos in memory
87
90
  @iterations = 0
@@ -435,8 +438,9 @@ module Clacky
435
438
  lines << "Parse failed: #{parse_error}"
436
439
  if parser_path
437
440
  expected_preview = "#{path}.preview.md"
441
+ interp = Utils::ParserManager.interpreter_for(File.basename(parser_path))
438
442
  lines << "Action required: fix the parser at #{parser_path}, then run:"
439
- lines << " ruby #{parser_path} #{path} > #{expected_preview}"
443
+ lines << " #{interp} #{parser_path} #{path} > #{expected_preview}"
440
444
  lines << "Once done, read #{expected_preview} to continue helping the user."
441
445
  end
442
446
  end
@@ -1005,6 +1009,14 @@ module Clacky
1005
1009
  # Inject working_dir so tools don't rely on Dir.chdir global state
1006
1010
  args[:working_dir] = @working_dir if @working_dir
1007
1011
 
1012
+ # Inject CLACKY_SESSION_ID into every terminal invocation so skills/system
1013
+ # prompts can reliably curl our own HTTP API (e.g. POST /api/ui/show_ext_refresh)
1014
+ # without the AI needing to know or guess the session id. Merges with (and never
1015
+ # overrides) any env the AI explicitly passed.
1016
+ if call[:name] == "terminal" && @session_id
1017
+ args[:env] = { "CLACKY_SESSION_ID" => @session_id.to_s }.merge(args[:env] || {})
1018
+ end
1019
+
1008
1020
  # For terminal: stream live stdout chunks to the UI as they arrive,
1009
1021
  # so the user sees real-time output (e.g. build logs) instead of a
1010
1022
  # blank spinner. The UI buffers lines for Ctrl+O fullscreen view
@@ -165,7 +165,8 @@ module Clacky
165
165
  :memory_update_enabled, :skill_evolution,
166
166
  :max_running_agents, :max_idle_agents,
167
167
  :default_working_dir,
168
- :proxy_url
168
+ :proxy_url,
169
+ :clacky_license_server
169
170
 
170
171
  def initialize(options = {})
171
172
  @permission_mode = validate_permission_mode(options[:permission_mode])
@@ -222,6 +223,7 @@ module Clacky
222
223
  # ignored — set proxy_url here to route Clacky's outbound HTTP through
223
224
  # a proxy. Leave nil to go direct.
224
225
  @proxy_url = options[:proxy_url]
226
+ @clacky_license_server = options[:clacky_license_server]
225
227
 
226
228
  # Per-session virtual model overlay.
227
229
  # When set, #current_model returns a *merged* hash (the resolved @models
@@ -414,7 +416,7 @@ module Clacky
414
416
  memory_update_enabled
415
417
  skill_evolution max_running_agents max_idle_agents
416
418
  default_working_dir
417
- proxy_url
419
+ proxy_url clacky_license_server
418
420
  ].freeze
419
421
 
420
422
  # Serialize the current agent configuration to YAML.
@@ -434,7 +436,8 @@ module Clacky
434
436
  "max_running_agents" => @max_running_agents,
435
437
  "max_idle_agents" => @max_idle_agents,
436
438
  "default_working_dir" => @default_working_dir,
437
- "proxy_url" => @proxy_url
439
+ "proxy_url" => @proxy_url,
440
+ "clacky_license_server" => @clacky_license_server
438
441
  }
439
442
  YAML.dump("settings" => settings, "models" => persistable_models)
440
443
  end
@@ -383,16 +383,16 @@ module Clacky
383
383
 
384
384
  # Returns true when a public distribution refresh is due.
385
385
  #
386
- # Refresh is needed only when the install has a package_name configured
387
- # but is not yet activated activated licenses already get fresh
388
- # distribution data via #heartbeat! (once per 24h).
386
+ # Refresh is needed for unactivated branded installs and for private
387
+ # platform sources that have not supplied their deployment brand yet.
388
+ # Activated licenses already get fresh distribution data via #heartbeat!.
389
389
  #
390
390
  # Rate limit: once every HEARTBEAT_INTERVAL (24h), measured from the last
391
391
  # SUCCESSFUL refresh. A failed refresh does not advance the clock so we'll
392
392
  # keep trying on subsequent startups / status polls.
393
393
  def distribution_refresh_due?
394
- return false unless branded?
395
394
  return false if activated?
395
+ return false unless branded? || private_platform_source?
396
396
  return true if @distribution_last_refreshed_at.nil?
397
397
 
398
398
  elapsed = Time.now.utc - @distribution_last_refreshed_at
@@ -400,7 +400,8 @@ module Clacky
400
400
  end
401
401
 
402
402
  # Refresh public brand assets (logo, theme, homepage_url, support_*) for
403
- # installs that have `package_name` configured but no activated license yet.
403
+ # unactivated installs. Package installs use an exact package lookup;
404
+ # private platform sources without a local brand use the source default.
404
405
  #
405
406
  # Motivation: `install.sh --brand-name=X --command=X` only writes
406
407
  # product_name + package_name to brand.yml. The rest of the distribution
@@ -409,32 +410,47 @@ module Clacky
409
410
  # anonymous public lookup endpoint.
410
411
  #
411
412
  # Behaviour:
412
- # * No-op (returns { success: false, message: "..." }) when not branded,
413
- # already activated, or package_name is blank.
413
+ # * No-op when already activated, or when neither a package_name nor a
414
+ # private platform source is available.
414
415
  # * On success: apply_distribution + save + stamp
415
416
  # @distribution_last_refreshed_at.
417
+ # * When a block is provided, discard the response unless it still
418
+ # confirms that the platform source is current.
416
419
  # * On failure: log and return without touching the timestamp (so we
417
420
  # retry on next trigger).
418
421
  #
419
422
  # Returns { success: Boolean, message: String }.
420
423
  def refresh_distribution!
421
- unless branded?
424
+ source_brand_lookup = private_platform_source? &&
425
+ (@package_name.nil? || @package_name.strip.empty?)
426
+ unless branded? || source_brand_lookup
422
427
  return { success: false, message: "Not branded" }
423
428
  end
424
429
  if activated?
425
430
  return { success: false, message: "License activated — use heartbeat! instead" }
426
431
  end
427
- if @package_name.nil? || @package_name.strip.empty?
432
+ if !source_brand_lookup && (@package_name.nil? || @package_name.strip.empty?)
428
433
  return { success: false, message: "package_name not configured" }
429
434
  end
430
435
 
431
- encoded_pkg = URI.encode_www_form_component(@package_name.strip)
432
- path = "/api/v1/distributions/lookup?package_name=#{encoded_pkg}"
436
+ if source_brand_lookup
437
+ path = "/api/v1/distributions/lookup"
438
+ else
439
+ encoded_pkg = URI.encode_www_form_component(@package_name.strip)
440
+ path = "/api/v1/distributions/lookup?package_name=#{encoded_pkg}"
441
+ end
433
442
 
434
- Clacky::Logger.info("[Brand] refresh_distribution! fetching — package_name=#{@package_name}")
443
+ Clacky::Logger.info(
444
+ "[Brand] refresh_distribution! fetching — package_name=#{@package_name || "source-default"}"
445
+ )
435
446
  response = platform_client.get(path)
436
447
 
437
448
  if response[:success] && response[:data].is_a?(Hash) && response[:data]["distribution"].is_a?(Hash)
449
+ if block_given? && !yield
450
+ Clacky::Logger.info("[Brand] refresh_distribution! discarded — platform source changed")
451
+ return { success: false, message: "Platform source changed during refresh" }
452
+ end
453
+
438
454
  apply_distribution(response[:data]["distribution"])
439
455
  @distribution_last_refreshed_at = Time.now.utc
440
456
  save
@@ -1762,6 +1778,10 @@ module Clacky
1762
1778
  @homepage_url = dist["homepage_url"] if dist.key?("homepage_url")
1763
1779
  end
1764
1780
 
1781
+ private def private_platform_source?
1782
+ !ENV["CLACKY_LICENSE_SERVER"].to_s.strip.empty?
1783
+ end
1784
+
1765
1785
  # Download a remote URL to a local file path.
1766
1786
  #
1767
1787
  # Deprecated: this method now delegates to
@@ -143,3 +143,18 @@ main product, and apply every rule below whenever you propose or write code:
143
143
  Ruby and carry supply-chain risk.
144
144
  - After editing `view.js`, `handler.rb`, or a `SKILL.md`, tell the user to reload the
145
145
  WebUI page — hot reload is per-request, no restart needed.
146
+ - After you finish editing extension files, call this once at the very end to trigger
147
+ a reload button in the UI (if it doesn't appear, the user can manually refresh the browser):
148
+ ```
149
+ curl -s --noproxy '*' -X POST "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/ui/show_ext_refresh" \
150
+ -H "Content-Type: application/json" \
151
+ -d "{\"session_id\": \"${CLACKY_SESSION_ID}\"}"
152
+ ```
153
+ (`--noproxy '*'` prevents shell proxy env vars from silently intercepting this loopback call.)
154
+ - If the extension contributes a `session.aside` panel, also call this right after the above
155
+ to open the panel automatically:
156
+ ```
157
+ curl -s --noproxy '*' -X POST "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/ui/open_aside" \
158
+ -H "Content-Type: application/json" \
159
+ -d "{\"session_id\": \"${CLACKY_SESSION_ID}\"}"
160
+ ```