samagotchi 0.22.0 → 0.23.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 +33 -1
- data/bin/chi +9 -4
- data/docs/configuration.md +4 -4
- data/docs/releasing.md +1 -1
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +4 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +2 -2
- data/lib/samagotchi/bridge/bounded_queue.rb +0 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -6
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -2
- data/lib/samagotchi/bridge.rb +12 -9
- data/lib/samagotchi/bridge_client/event_stream.rb +2 -2
- data/lib/samagotchi/bridge_client.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +39 -29
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +2 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +2 -0
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +6 -5
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +2 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +5 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -7
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cancellation_controller.rb +1 -1
- data/lib/samagotchi/cli/command.rb +1 -1
- data/lib/samagotchi/client.rb +3 -5
- data/lib/samagotchi/commands/registry.rb +2 -2
- data/lib/samagotchi/config.rb +38 -14
- data/lib/samagotchi/context_note.rb +1 -1
- data/lib/samagotchi/context_status.rb +6 -6
- data/lib/samagotchi/continue_offer.rb +15 -8
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/desktop/macos.rb +5 -4
- data/lib/samagotchi/empty_answer_retry.rb +1 -1
- data/lib/samagotchi/engine.rb +17 -17
- data/lib/samagotchi/gem_update.rb +2 -1
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +5 -5
- data/lib/samagotchi/guardrails/approvals.rb +2 -2
- data/lib/samagotchi/guardrails/gate.rb +2 -2
- data/lib/samagotchi/guardrails/read_only_shell.rb +5 -3
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +2 -2
- data/lib/samagotchi/guardrails/shell_paths.rb +2 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -6
- data/lib/samagotchi/hooks/loader.rb +18 -16
- data/lib/samagotchi/hooks/registry.rb +10 -2
- data/lib/samagotchi/host_registry.rb +1 -3
- data/lib/samagotchi/idle_recap.rb +18 -15
- data/lib/samagotchi/idle_reminders.rb +6 -3
- data/lib/samagotchi/image_store.rb +5 -2
- data/lib/samagotchi/kernel_loop.rb +31 -31
- data/lib/samagotchi/llm/chat_loop.rb +9 -6
- data/lib/samagotchi/llm/errors.rb +2 -2
- data/lib/samagotchi/llm/http.rb +12 -12
- data/lib/samagotchi/llm/openai_chat.rb +39 -28
- data/lib/samagotchi/llm/prompt_cache.rb +1 -1
- data/lib/samagotchi/log.rb +2 -2
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/memory_bundle/builder.rb +9 -3
- data/lib/samagotchi/memory_bundle/index_updater.rb +13 -1
- data/lib/samagotchi/memory_bundle/installer.rb +67 -57
- data/lib/samagotchi/memory_bundle/listing.rb +5 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +6 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +16 -13
- data/lib/samagotchi/memory_bundle/source.rb +7 -3
- data/lib/samagotchi/memory_bundle/status.rb +3 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +3 -2
- data/lib/samagotchi/memory_bundle/trash.rb +5 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +9 -6
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/model_overlay.rb +3 -5
- data/lib/samagotchi/model_profile.rb +12 -8
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/output_formatter.rb +18 -17
- data/lib/samagotchi/owner_lock.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +8 -5
- data/lib/samagotchi/plugin/context.rb +1 -3
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/plugin/service.rb +1 -1
- data/lib/samagotchi/plugin/sessions.rb +1 -0
- data/lib/samagotchi/plugin_tasks.rb +1 -1
- data/lib/samagotchi/prompt.rb +68 -12
- data/lib/samagotchi/prompt_literal_guard.rb +1 -1
- data/lib/samagotchi/question_desk.rb +7 -7
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/relay_desk.rb +1 -1
- data/lib/samagotchi/reminder_store.rb +2 -1
- data/lib/samagotchi/send_command.rb +4 -2
- data/lib/samagotchi/session.rb +2 -2
- data/lib/samagotchi/session_archive_command.rb +5 -1
- data/lib/samagotchi/session_commands.rb +1 -3
- data/lib/samagotchi/session_manager.rb +2 -4
- data/lib/samagotchi/session_metrics.rb +10 -10
- data/lib/samagotchi/session_observer.rb +3 -2
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/sessions_command.rb +14 -5
- data/lib/samagotchi/system_prompt.rb +19 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +1 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +10 -10
- data/lib/samagotchi/terminal_ui/input_support.rb +2 -2
- data/lib/samagotchi/terminal_ui/line_reader.rb +3 -3
- data/lib/samagotchi/terminal_ui/question_prompt.rb +23 -23
- data/lib/samagotchi/terminal_ui/question_slot.rb +10 -10
- data/lib/samagotchi/terminal_ui/reline_seam.rb +1 -1
- data/lib/samagotchi/terminal_ui/repl_input.rb +3 -2
- data/lib/samagotchi/terminal_ui.rb +9 -5
- data/lib/samagotchi/text_diff.rb +12 -12
- data/lib/samagotchi/thinking.rb +6 -2
- data/lib/samagotchi/thought_stream_splitter.rb +6 -3
- data/lib/samagotchi/tool_activity.rb +1 -0
- data/lib/samagotchi/tool_call_parser.rb +13 -14
- data/lib/samagotchi/tool_declarations.rb +36 -35
- data/lib/samagotchi/tools/ask_user_question.rb +3 -4
- data/lib/samagotchi/tools/builtin_calls.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +0 -1
- data/lib/samagotchi/tools/edit.rb +14 -13
- data/lib/samagotchi/tools/execute.rb +3 -5
- data/lib/samagotchi/tools/list_reminders.rb +1 -1
- data/lib/samagotchi/tools/memory.rb +21 -22
- data/lib/samagotchi/tools/read.rb +7 -7
- data/lib/samagotchi/tools/task_create.rb +1 -1
- data/lib/samagotchi/tools/task_get.rb +1 -1
- data/lib/samagotchi/tools/task_list.rb +1 -1
- data/lib/samagotchi/tools/task_runtime.rb +8 -9
- data/lib/samagotchi/tools/task_stop.rb +1 -1
- data/lib/samagotchi/tools/task_wait.rb +3 -4
- data/lib/samagotchi/tools/web_fetch.rb +2 -4
- data/lib/samagotchi/tools/write.rb +1 -1
- data/lib/samagotchi/turn_flow.rb +8 -8
- data/lib/samagotchi/turn_note.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +2 -2
- data/lib/samagotchi/web/app.rb +13 -6
- data/lib/samagotchi/web/markdown_renderer.rb +2 -1
- data/lib/samagotchi/web/qr.rb +7 -1
- data/lib/samagotchi/web/server.rb +6 -2
- data/lib/samagotchi/worker.rb +10 -2
- metadata +20 -20
|
@@ -64,6 +64,7 @@ module Samagotchi
|
|
|
64
64
|
def self.read(dir:)
|
|
65
65
|
manifest_path = File.join(dir, "manifest.yml")
|
|
66
66
|
raise ValidationError, "manifest.yml not found in #{dir}" unless File.exist?(manifest_path)
|
|
67
|
+
|
|
67
68
|
new(path: manifest_path)
|
|
68
69
|
end
|
|
69
70
|
|
|
@@ -72,6 +73,7 @@ module Samagotchi
|
|
|
72
73
|
def checksum_for(file_key)
|
|
73
74
|
raw = @files[file_key]
|
|
74
75
|
return nil unless raw
|
|
76
|
+
|
|
75
77
|
raw.start_with?("sha256:") ? raw[7..] : raw
|
|
76
78
|
end
|
|
77
79
|
|
|
@@ -79,8 +81,10 @@ module Samagotchi
|
|
|
79
81
|
def checksum_for_hook(basename)
|
|
80
82
|
raw = @hooks[basename]
|
|
81
83
|
return nil unless raw
|
|
84
|
+
|
|
82
85
|
sha = raw[:sha256] || raw["sha256"]
|
|
83
86
|
return nil unless sha
|
|
87
|
+
|
|
84
88
|
sha.to_s.start_with?("sha256:") ? sha.to_s[7..] : sha.to_s
|
|
85
89
|
end
|
|
86
90
|
|
|
@@ -99,18 +103,18 @@ module Samagotchi
|
|
|
99
103
|
return nil if Gem::Requirement.new(*requirement.to_s.split(",").map(&:strip)).satisfied_by?(Gem::Version.new(version.to_s))
|
|
100
104
|
|
|
101
105
|
"it requires chi #{requirement} (this is chi #{version})"
|
|
102
|
-
rescue ArgumentError
|
|
106
|
+
rescue ArgumentError => e # Gem::Requirement::BadRequirementError is one
|
|
103
107
|
"its requires_chi #{requirement.inspect} is not a version requirement (#{e.message})"
|
|
104
108
|
end
|
|
105
109
|
|
|
106
110
|
# Writes a fresh manifest.yml from computed checksums at `dir`.
|
|
107
|
-
def self.write(dir:, name:, version:, scope: nil, description: "",
|
|
111
|
+
def self.write(dir:, name:, version:, files:, scope: nil, description: "", hooks: nil, trust_level: nil,
|
|
108
112
|
plugin: nil, requires_chi: nil, needs: nil)
|
|
109
113
|
FileUtils.mkdir_p(dir)
|
|
110
114
|
manifest = {
|
|
111
115
|
"name" => name,
|
|
112
116
|
"version" => version,
|
|
113
|
-
"description" => description
|
|
117
|
+
"description" => description
|
|
114
118
|
}
|
|
115
119
|
manifest["scope"] = scope if scope
|
|
116
120
|
manifest["files"] = files # { "file.md" => "sha256:abc..." }
|
|
@@ -167,19 +171,23 @@ module Samagotchi
|
|
|
167
171
|
def require_str(hash, key)
|
|
168
172
|
val = hash[key]
|
|
169
173
|
return val if val.is_a?(String) && !val.empty?
|
|
174
|
+
|
|
170
175
|
raise ValidationError, "manifest missing required field: #{key}"
|
|
171
176
|
end
|
|
172
177
|
|
|
173
178
|
def normalize_scope(val)
|
|
174
179
|
return nil if val.nil?
|
|
180
|
+
|
|
175
181
|
v = val.to_s.strip.downcase
|
|
176
182
|
%w[project system].include?(v) ? v : nil
|
|
177
183
|
end
|
|
178
184
|
|
|
179
185
|
def parse_files(raw)
|
|
180
186
|
return {} unless raw.is_a?(Hash)
|
|
187
|
+
|
|
181
188
|
raw.each_with_object({}) do |(k, v), acc|
|
|
182
189
|
next unless k.is_a?(String) && !k.empty?
|
|
190
|
+
|
|
183
191
|
str = v.to_s
|
|
184
192
|
acc[k] = if str.start_with?("sha256:")
|
|
185
193
|
str
|
|
@@ -234,11 +242,13 @@ module Samagotchi
|
|
|
234
242
|
|
|
235
243
|
def parse_hooks(raw)
|
|
236
244
|
return {} unless raw.is_a?(Hash)
|
|
245
|
+
|
|
237
246
|
raw.each_with_object({}) do |(k, v), acc|
|
|
238
247
|
next unless k.is_a?(String) && !k.empty?
|
|
239
248
|
next unless v.is_a?(Hash)
|
|
249
|
+
|
|
240
250
|
sha = (v["sha256"] || v[:sha256] || "").to_s
|
|
241
|
-
sha =
|
|
251
|
+
sha = "sha256:#{sha}" unless sha.empty? || sha.start_with?("sha256:")
|
|
242
252
|
event = (v["event"] || v[:event] || "").to_s.strip
|
|
243
253
|
on_error = (v["on_error"] || v[:on_error] || "skip").to_s.strip
|
|
244
254
|
on_error = "skip" if on_error.empty?
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "digest"
|
|
3
4
|
|
|
4
5
|
module Samagotchi
|
|
@@ -33,18 +34,19 @@ module Samagotchi
|
|
|
33
34
|
incoming_digest = Digest::SHA256.hexdigest(File.read(incoming_path))
|
|
34
35
|
|
|
35
36
|
if current_digest == incoming_digest
|
|
36
|
-
|
|
37
|
+
:noop
|
|
37
38
|
elsif current_digest == base_digest
|
|
38
|
-
|
|
39
|
+
:fast_forward
|
|
39
40
|
elsif incoming_digest == base_digest
|
|
40
|
-
|
|
41
|
+
:keep
|
|
41
42
|
else
|
|
42
|
-
|
|
43
|
+
:conflict
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def self.current_modified?(base_path, current_path)
|
|
47
48
|
return true unless base_path && File.exist?(base_path) && File.exist?(current_path)
|
|
49
|
+
|
|
48
50
|
Digest::SHA256.hexdigest(File.read(base_path)) != Digest::SHA256.hexdigest(File.read(current_path))
|
|
49
51
|
end
|
|
50
52
|
end
|
|
@@ -154,11 +154,11 @@ module Samagotchi
|
|
|
154
154
|
if existing && existing[:files]
|
|
155
155
|
existing[:files].keys.each do |old_key|
|
|
156
156
|
old_key_str = old_key.to_s
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
157
|
+
next if files.key?(old_key_str) || files.key?(old_key)
|
|
158
|
+
|
|
159
|
+
old_base = File.join(bases_dir, old_key_str)
|
|
160
|
+
FileUtils.rm_f(old_base)
|
|
161
|
+
merged_entries.delete(old_key_str)
|
|
162
162
|
end
|
|
163
163
|
end
|
|
164
164
|
|
|
@@ -172,9 +172,10 @@ module Samagotchi
|
|
|
172
172
|
hooks.each do |k, v|
|
|
173
173
|
next unless k.is_a?(String) && !k.empty?
|
|
174
174
|
next unless v.is_a?(Hash)
|
|
175
|
+
|
|
175
176
|
hv = v.transform_keys(&:to_sym)
|
|
176
177
|
sha = (hv[:sha256] || "").to_s
|
|
177
|
-
sha =
|
|
178
|
+
sha = "sha256:#{sha}" unless sha.empty? || sha.start_with?("sha256:")
|
|
178
179
|
normalized_hooks[k] = {
|
|
179
180
|
sha256: sha,
|
|
180
181
|
event: (hv[:event] || "").to_s,
|
|
@@ -189,7 +190,7 @@ module Samagotchi
|
|
|
189
190
|
old_key_str = old_key.to_s
|
|
190
191
|
unless normalized_hooks.key?(old_key_str) || normalized_hooks.key?(old_key_str.to_sym)
|
|
191
192
|
old_base = File.join(bases_dir, old_key_str)
|
|
192
|
-
FileUtils.rm_f(old_base)
|
|
193
|
+
FileUtils.rm_f(old_base)
|
|
193
194
|
end
|
|
194
195
|
end
|
|
195
196
|
end
|
|
@@ -204,12 +205,12 @@ module Samagotchi
|
|
|
204
205
|
meta = meta.merge(sha256: "sha256:#{Digest::SHA256.hexdigest(File.read(src.to_s))}")
|
|
205
206
|
end
|
|
206
207
|
hooks_map[k] = meta.transform_keys(&:to_s)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
next unless src && File.exist?(src.to_s)
|
|
209
|
+
|
|
210
|
+
begin
|
|
211
|
+
File.write(File.join(bases_dir, k), File.read(src.to_s))
|
|
212
|
+
rescue StandardError
|
|
213
|
+
nil
|
|
213
214
|
end
|
|
214
215
|
end
|
|
215
216
|
|
|
@@ -256,6 +257,7 @@ module Samagotchi
|
|
|
256
257
|
conflicts.each do |file_key, info|
|
|
257
258
|
key = file_key.to_s
|
|
258
259
|
next unless raw["files"].key?(key)
|
|
260
|
+
|
|
259
261
|
src = [info[:incoming], info[:current]].find { |p| p && File.exist?(p) }
|
|
260
262
|
next unless src
|
|
261
263
|
|
|
@@ -270,6 +272,7 @@ module Samagotchi
|
|
|
270
272
|
def read
|
|
271
273
|
manifest_path = File.join(@bundle_dir, "manifest.json")
|
|
272
274
|
return nil unless File.exist?(manifest_path)
|
|
275
|
+
|
|
273
276
|
JSON.parse(File.read(manifest_path), symbolize_names: true)
|
|
274
277
|
end
|
|
275
278
|
|
|
@@ -28,7 +28,7 @@ module Samagotchi
|
|
|
28
28
|
lower = path.downcase
|
|
29
29
|
if lower.end_with?(".zip")
|
|
30
30
|
normalize_zip(path)
|
|
31
|
-
elsif lower.end_with?(".tar.gz"
|
|
31
|
+
elsif lower.end_with?(".tar.gz", ".tgz", ".tar")
|
|
32
32
|
normalize_tar(path)
|
|
33
33
|
else
|
|
34
34
|
ext = File.extname(path).downcase
|
|
@@ -57,6 +57,7 @@ module Samagotchi
|
|
|
57
57
|
def self.git_url?(str)
|
|
58
58
|
s = str.to_s.strip
|
|
59
59
|
return false if s.empty?
|
|
60
|
+
|
|
60
61
|
s.match?(%r{\A(?:https?://|git@|ssh://|git://|file://).+})
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -78,6 +79,7 @@ module Samagotchi
|
|
|
78
79
|
args += [url, clone_dir]
|
|
79
80
|
result = system(*args)
|
|
80
81
|
raise UnknownSourceError, "git clone failed for #{git_url}" unless result && File.directory?(clone_dir)
|
|
82
|
+
|
|
81
83
|
# Capture HEAD commit before stripping .git (audit trail)
|
|
82
84
|
commit = nil
|
|
83
85
|
begin
|
|
@@ -103,12 +105,11 @@ module Samagotchi
|
|
|
103
105
|
end
|
|
104
106
|
end
|
|
105
107
|
|
|
106
|
-
private
|
|
107
|
-
|
|
108
108
|
def self.normalize_zip(zip_path)
|
|
109
109
|
Dir.mktmpdir("samagotchi-zip-") do |extract_dir|
|
|
110
110
|
result = system("unzip", "-o", zip_path, "-d", extract_dir)
|
|
111
111
|
raise UnknownSourceError, "unzip failed for #{zip_path}" unless result
|
|
112
|
+
|
|
112
113
|
clean_copy_of(extract_dir)
|
|
113
114
|
end
|
|
114
115
|
end
|
|
@@ -119,6 +120,7 @@ module Samagotchi
|
|
|
119
120
|
cmd = is_gz ? ["tar", "-xzf"] : ["tar", "-xf"]
|
|
120
121
|
result = system(*cmd, tar_path, "-C", extract_dir)
|
|
121
122
|
raise UnknownSourceError, "tar extraction failed for #{tar_path}" unless result
|
|
123
|
+
|
|
122
124
|
clean_copy_of(extract_dir)
|
|
123
125
|
end
|
|
124
126
|
end
|
|
@@ -150,6 +152,8 @@ module Samagotchi
|
|
|
150
152
|
end
|
|
151
153
|
[dst, true]
|
|
152
154
|
end
|
|
155
|
+
|
|
156
|
+
private_class_method :normalize_zip, :normalize_tar, :clean_copy_of
|
|
153
157
|
end
|
|
154
158
|
end
|
|
155
159
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "digest"
|
|
3
4
|
require_relative "provenance"
|
|
4
5
|
require_relative "manifest"
|
|
@@ -20,7 +21,7 @@ module Samagotchi
|
|
|
20
21
|
return nil unless data
|
|
21
22
|
|
|
22
23
|
raw_scope = scope_override || data[:scope]&.to_s
|
|
23
|
-
scope =
|
|
24
|
+
scope = raw_scope.nil? || raw_scope.strip.empty? ? "system" : raw_scope
|
|
24
25
|
target_dir = MemoryPaths.scope_dir(scope)
|
|
25
26
|
files = data[:files] || {}
|
|
26
27
|
details = {}
|
|
@@ -108,6 +109,7 @@ module Samagotchi
|
|
|
108
109
|
def self.index_has_entry?(scope, file_key)
|
|
109
110
|
path = IndexUpdater.index_path_for(scope)
|
|
110
111
|
return false unless path && File.exist?(path)
|
|
112
|
+
|
|
111
113
|
content = File.read(path)
|
|
112
114
|
[file_key.delete_suffix(".md"), file_key].uniq.any? { |name| content.match?(IndexUpdater.managed_pattern(name)) }
|
|
113
115
|
end
|
|
@@ -110,12 +110,13 @@ module Samagotchi
|
|
|
110
110
|
|
|
111
111
|
def skip?
|
|
112
112
|
val = ENV[SKIP_ENV].to_s.strip.downcase
|
|
113
|
-
|
|
113
|
+
%w[1 true].include?(val)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
# Unparseable versions fall back to the old "any difference upgrades" behaviour.
|
|
117
117
|
def installed_newer?(installed, shipped)
|
|
118
|
-
installed
|
|
118
|
+
installed = installed.to_s
|
|
119
|
+
shipped = shipped.to_s
|
|
119
120
|
return false unless Gem::Version.correct?(installed) && Gem::Version.correct?(shipped)
|
|
120
121
|
|
|
121
122
|
Gem::Version.new(installed) > Gem::Version.new(shipped)
|
|
@@ -44,7 +44,7 @@ module Samagotchi
|
|
|
44
44
|
def self.empty!(bundles_dir: MemoryPaths.bundles_dir, older_than_days: nil, now: Time.now, dry_run: false)
|
|
45
45
|
entries = self.entries(bundles_dir: bundles_dir, now: now)
|
|
46
46
|
if older_than_days
|
|
47
|
-
cutoff = now - older_than_days.to_i * 86_400
|
|
47
|
+
cutoff = now - (older_than_days.to_i * 86_400)
|
|
48
48
|
entries = entries.select { |e| e.time < cutoff }
|
|
49
49
|
end
|
|
50
50
|
return entries if dry_run
|
|
@@ -98,7 +98,7 @@ module Samagotchi
|
|
|
98
98
|
File.directory?(path) && !File.symlink?(path)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
def self.entry_for(trash_dir, name,
|
|
101
|
+
def self.entry_for(trash_dir, name, _now)
|
|
102
102
|
path = File.join(trash_dir, name)
|
|
103
103
|
time = parse_timestamp(name) || File.mtime(path)
|
|
104
104
|
files, bytes = count_files(path)
|
|
@@ -126,11 +126,14 @@ module Samagotchi
|
|
|
126
126
|
bytes = 0
|
|
127
127
|
Dir.glob(File.join(dir, "**", "*"), File::FNM_DOTMATCH).each do |f|
|
|
128
128
|
next if File.directory?(f)
|
|
129
|
+
|
|
129
130
|
files += 1
|
|
130
131
|
bytes += File.size(f) rescue 0
|
|
131
132
|
end
|
|
132
133
|
[files, bytes]
|
|
133
134
|
end
|
|
135
|
+
|
|
136
|
+
private_class_method :real_dir?, :entry_for, :parse_timestamp, :count_files
|
|
134
137
|
end
|
|
135
138
|
end
|
|
136
139
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "fileutils"
|
|
3
4
|
require "digest"
|
|
4
5
|
require_relative "../memory_paths"
|
|
@@ -38,7 +39,7 @@ module Samagotchi
|
|
|
38
39
|
# Determine scope from flag or provenance
|
|
39
40
|
cli_scope = @scope.to_s.strip.empty? ? nil : @scope
|
|
40
41
|
raw = cli_scope || data[:scope]&.to_s
|
|
41
|
-
target_scope =
|
|
42
|
+
target_scope = raw.nil? || raw.strip.empty? ? "system" : raw
|
|
42
43
|
target_dir = MemoryPaths.scope_dir(target_scope) or raise UninstallError, "invalid scope: #{target_scope}"
|
|
43
44
|
|
|
44
45
|
files = data[:files] || {}
|
|
@@ -55,19 +56,20 @@ module Samagotchi
|
|
|
55
56
|
end.reject { |_, others| others.empty? }
|
|
56
57
|
|
|
57
58
|
blocked = []
|
|
58
|
-
files.each do |file_key,
|
|
59
|
+
files.each do |file_key, _meta|
|
|
59
60
|
file_key_str = file_key.to_s
|
|
60
61
|
target_path = File.join(target_dir, file_key_str)
|
|
61
62
|
base_path = provenance.base_path(file_key_str)
|
|
62
63
|
next unless File.exist?(target_path)
|
|
63
64
|
next if shared.key?(file_key_str)
|
|
65
|
+
|
|
64
66
|
if !@force && File.exist?(base_path) && Merger.current_modified?(base_path, target_path)
|
|
65
67
|
blocked << file_key_str
|
|
66
68
|
@warnings << "Skipped #{file_key_str}: local edits detected (use --force to remove)"
|
|
67
69
|
end
|
|
68
70
|
end
|
|
69
71
|
|
|
70
|
-
raise UninstallError, "Uninstall blocked: #{blocked.join(
|
|
72
|
+
raise UninstallError, "Uninstall blocked: #{blocked.join(", ")} has local edits (use --force)" if blocked.any?
|
|
71
73
|
|
|
72
74
|
files.each do |file_key, _meta|
|
|
73
75
|
file_key_str = file_key.to_s
|
|
@@ -108,10 +110,11 @@ module Samagotchi
|
|
|
108
110
|
end
|
|
109
111
|
end
|
|
110
112
|
# Remove hooks dir if empty, else rm_rf will clean
|
|
111
|
-
FileUtils.rm_rf(hooks_dir)
|
|
113
|
+
FileUtils.rm_rf(hooks_dir)
|
|
112
114
|
else
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
+
# A stale hooks dir goes even when provenance has no hooks map
|
|
116
|
+
# (rm_rf ignores a missing dir).
|
|
117
|
+
FileUtils.rm_rf(provenance.hooks_dir)
|
|
115
118
|
end
|
|
116
119
|
|
|
117
120
|
plugin_path = provenance.plugin_path(data)
|
|
@@ -32,10 +32,7 @@ module Samagotchi
|
|
|
32
32
|
|
|
33
33
|
key = model_name.to_s
|
|
34
34
|
.downcase
|
|
35
|
-
.gsub(/[^a-z0-9]+/, "-")
|
|
36
|
-
.gsub(/-+/, "-")
|
|
37
|
-
.sub(/\A-/, "")
|
|
38
|
-
.sub(/-\z/, "")
|
|
35
|
+
.gsub(/[^a-z0-9]+/, "-").squeeze("-").delete_prefix("-").delete_suffix("-")
|
|
39
36
|
key.empty? ? nil : key
|
|
40
37
|
end
|
|
41
38
|
|
|
@@ -43,12 +40,13 @@ module Samagotchi
|
|
|
43
40
|
# Respects MemoryBundle overrides (delegates to Tools::MemoryRead.memories_dir).
|
|
44
41
|
def self.overlay_path_for(name, key, scope)
|
|
45
42
|
return nil unless key && !key.to_s.strip.empty?
|
|
43
|
+
|
|
46
44
|
File.join(Tools::MemoryRead.memories_dir(scope), "#{name}.#{key.to_s.strip}.md")
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
# Regex matching overlay file names: /<something>.<lowercase-dashed-key>.md\z
|
|
50
48
|
# Useful for tooling, tests, and future index suppression.
|
|
51
|
-
OVERLAY_SUFFIX_PATTERN = /\.([a-z0-9
|
|
49
|
+
OVERLAY_SUFFIX_PATTERN = /\.([a-z0-9-]+)\.md\z/
|
|
52
50
|
|
|
53
51
|
# The base file name (`tips.md`) an overlay-looking name
|
|
54
52
|
# (`tips.qwen3.md`) would belong to, or nil when it doesn't match the
|
|
@@ -17,10 +17,13 @@ module Samagotchi
|
|
|
17
17
|
MODEL_FROM_CLI_ENV = "SAMAGOTCHI_DEFAULT_MODEL_FROM_CLI"
|
|
18
18
|
# No model anywhere (--model, default.model in config.yml, the env): a
|
|
19
19
|
# first run before any config. An ArgumentError, as before.
|
|
20
|
-
MissingModel
|
|
20
|
+
class MissingModel < ArgumentError
|
|
21
|
+
end
|
|
22
|
+
|
|
21
23
|
# A host-qualified model whose host isn't configured (a MissingModel,
|
|
22
24
|
# so every surface that reports a missing model reports it the same way).
|
|
23
|
-
UnknownHost
|
|
25
|
+
class UnknownHost < MissingModel
|
|
26
|
+
end
|
|
24
27
|
|
|
25
28
|
# Gemma 4 thinks in a channel: `<|channel>thought` … `<channel|>`. The
|
|
26
29
|
# one source for the parser, the stream splitter, the literal guard and
|
|
@@ -74,7 +77,7 @@ module Samagotchi
|
|
|
74
77
|
new(
|
|
75
78
|
name: "gemma4",
|
|
76
79
|
turn_start: "<|turn>",
|
|
77
|
-
turn_end: "<
|
|
80
|
+
turn_end: "<turn|>",
|
|
78
81
|
tool_call_open: "<|tool_call>",
|
|
79
82
|
tool_call_close: "<tool_call|>",
|
|
80
83
|
tool_response_open: "<|tool_response>",
|
|
@@ -88,7 +91,7 @@ module Samagotchi
|
|
|
88
91
|
user_prefix: "",
|
|
89
92
|
assistant_prefix: "",
|
|
90
93
|
model_prefix: "",
|
|
91
|
-
stop_sequences: ["<
|
|
94
|
+
stop_sequences: ["<turn|>", "<|tool_response>"],
|
|
92
95
|
tool_decl_format: :gemma4
|
|
93
96
|
)
|
|
94
97
|
end
|
|
@@ -165,14 +168,15 @@ module Samagotchi
|
|
|
165
168
|
if !parsed.host_name && (disabled = disabled_host_prefix(ref, hosts, env))
|
|
166
169
|
raise UnknownHost, "host '#{disabled}' is disabled (enabled: false in config.yml)"
|
|
167
170
|
end
|
|
171
|
+
|
|
168
172
|
warn_host_slash(ref, hosts) unless parsed.host_name
|
|
169
173
|
host = Samagotchi::ConfigFile.unknown_host_prefix(ref, hosts: hosts)
|
|
170
174
|
return model_name unless host
|
|
171
175
|
|
|
172
176
|
names = hosts.keys.map { |k| k.to_s.downcase }.sort
|
|
173
177
|
near = Samagotchi::Config.near_names(host, names).first(3)
|
|
174
|
-
hint = near.empty? ? "" : " (did you mean #{near.map { |n| "'#{n}'" }.join(
|
|
175
|
-
raise UnknownHost, "unknown host '#{host}' in model '#{ref}'#{hint}; the configured hosts are #{names.join(
|
|
178
|
+
hint = near.empty? ? "" : " (did you mean #{near.map { |n| "'#{n}'" }.join(" or ")}?)"
|
|
179
|
+
raise UnknownHost, "unknown host '#{host}' in model '#{ref}'#{hint}; the configured hosts are #{names.join(", ")}"
|
|
176
180
|
end
|
|
177
181
|
|
|
178
182
|
# The prefix of "box:x" when box is a host config.yml has with
|
|
@@ -275,7 +279,7 @@ module Samagotchi
|
|
|
275
279
|
FINGERPRINTS.each do |name, markers|
|
|
276
280
|
return [name, markers.join(" + ")] if markers.all? { |marker| template.include?(marker) }
|
|
277
281
|
end
|
|
278
|
-
return [
|
|
282
|
+
return %w[qwen36 ChatML] if template.include?("<|im_start|>")
|
|
279
283
|
|
|
280
284
|
nil
|
|
281
285
|
end
|
|
@@ -341,7 +345,7 @@ module Samagotchi
|
|
|
341
345
|
|
|
342
346
|
def self.configured(value, where)
|
|
343
347
|
profile = named(value)
|
|
344
|
-
Log.warn(:config, "unknown_profile", echo: "Warning: unknown profile #{value.to_s.inspect} in #{where} (allowed: #{NAMES.join(
|
|
348
|
+
Log.warn(:config, "unknown_profile", echo: "Warning: unknown profile #{value.to_s.inspect} in #{where} (allowed: #{NAMES.join(", ")}) — ignored", profile: value.to_s, where: where) unless profile
|
|
345
349
|
profile
|
|
346
350
|
end
|
|
347
351
|
private_class_method :configured
|
|
@@ -92,6 +92,7 @@ module Samagotchi
|
|
|
92
92
|
# passes down may never close: waiting on either would hang a script.
|
|
93
93
|
def read_stdin
|
|
94
94
|
return nil if @stdin.respond_to?(:tty?) && @stdin.tty?
|
|
95
|
+
|
|
95
96
|
if @stdin.respond_to?(:stat)
|
|
96
97
|
stat = @stdin.stat
|
|
97
98
|
return nil unless stat.pipe? || stat.file?
|
|
@@ -135,7 +136,7 @@ module Samagotchi
|
|
|
135
136
|
if owner
|
|
136
137
|
@stdout.puts("#{short} queued: its worker adds it within a few seconds")
|
|
137
138
|
else
|
|
138
|
-
queued = SessionInbox.find_new_note_files(File.dirname(
|
|
139
|
+
queued = SessionInbox.find_new_note_files(File.dirname(path, 2)).size
|
|
139
140
|
@stdout.puts("#{short} waits for the session's next start (#{queued} #{queued == 1 ? "note" : "notes"} queued)")
|
|
140
141
|
end
|
|
141
142
|
true
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Samagotchi
|
|
3
4
|
# OutputFormatter strips model wire-format tokens from raw engine output at
|
|
4
5
|
# render time, so downstream renderers (web UI, terminal UI) never show the
|
|
@@ -23,7 +24,6 @@ module Samagotchi
|
|
|
23
24
|
# wrapper forms; bare <name> content (no pipe) is deliberately preserved.
|
|
24
25
|
CONTROL_NAMES = %w[
|
|
25
26
|
turn
|
|
26
|
-
end_of_turn
|
|
27
27
|
tool_call
|
|
28
28
|
tool_response
|
|
29
29
|
tool
|
|
@@ -40,31 +40,32 @@ module Samagotchi
|
|
|
40
40
|
# <word> in the surrounding content is NOT stripped:
|
|
41
41
|
# <|name> / <name|> / <|name|>
|
|
42
42
|
INDIVIDUAL_RE = Regexp.new(
|
|
43
|
-
'<\\|(?:' + CONTROL_NAMES.join(
|
|
44
|
-
|
|
43
|
+
'<\\|(?:' + CONTROL_NAMES.join("|") + ')\\|?>' \
|
|
44
|
+
"|<(?:" + CONTROL_NAMES.join("|") + ')\\|>'
|
|
45
45
|
)
|
|
46
46
|
INDIVIDUAL_RE.freeze
|
|
47
47
|
|
|
48
48
|
# Angle-bracketed literal tokens stripped wholesale (including the thought
|
|
49
49
|
# channel's trailing word). Applied before INDIVIDUAL_RE so multi-part tokens
|
|
50
50
|
# are removed as a unit. Pipes are escaped; angle brackets are literal.
|
|
51
|
-
LITERALS_RE = /<\|"
|
|
51
|
+
LITERALS_RE = /<\|"\|>/
|
|
52
52
|
|
|
53
53
|
# Gemma 4's thought channel, as a whole block: `<|channel>thought` to its
|
|
54
54
|
# `<channel|>` close, or to the end of the text when the close is missing
|
|
55
55
|
# (ToolCallParser::Gemma strips the same pair). Removing only the marker
|
|
56
56
|
# would leave the model's private reasoning in the answer; the channel is
|
|
57
57
|
# the profile's thinking lane, not output.
|
|
58
|
-
GEMMA_THOUGHT_RE = /<\|\w*channel>thought.*?(?:<channel\|>|\z)/m
|
|
58
|
+
GEMMA_THOUGHT_RE = /<\|\w*channel>thought.*?(?:<channel\|>|\z)/m
|
|
59
59
|
|
|
60
60
|
# Qwen prompt-literal placeholders, e.g. </think>.
|
|
61
|
-
PROMPT_LITERALS = /\[\[SAMAGOTCHI_LITERAL_[A-Z_]+\]\]
|
|
61
|
+
PROMPT_LITERALS = /\[\[SAMAGOTCHI_LITERAL_[A-Z_]+\]\]/
|
|
62
62
|
|
|
63
63
|
# Qwen3.6 thinking blocks: <think>...</think> (including empty)
|
|
64
|
-
QWEN_THINK_RE =
|
|
64
|
+
QWEN_THINK_RE = %r{<think>.*?</think>}m
|
|
65
65
|
# Qwen tool_call XML blocks that survive when a tool call is rendered as text
|
|
66
66
|
# e.g. <tool_call>\n<function=list_reminders>\n</function>\n</tool_call>
|
|
67
|
-
QWEN_TOOL_CALL_RE =
|
|
67
|
+
QWEN_TOOL_CALL_RE = %r{<tool_call>.*?</tool_call>}m
|
|
68
|
+
|
|
68
69
|
module_function
|
|
69
70
|
|
|
70
71
|
# Remove both control-token and prompt-literal token families from +text+.
|
|
@@ -79,7 +80,7 @@ module Samagotchi
|
|
|
79
80
|
.gsub(/[ \t]+/, " ")
|
|
80
81
|
.gsub(/ *\n */, "\n")
|
|
81
82
|
.strip
|
|
82
|
-
cleaned.empty? ?
|
|
83
|
+
cleaned.empty? ? "" : cleaned
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
# The same tokens as #strip, but the text keeps its layout (indentation,
|
|
@@ -91,15 +92,15 @@ module Samagotchi
|
|
|
91
92
|
|
|
92
93
|
def remove_tokens(text)
|
|
93
94
|
text.to_s
|
|
94
|
-
.gsub(QWEN_THINK_RE,
|
|
95
|
-
.gsub(GEMMA_THOUGHT_RE,
|
|
96
|
-
.gsub(QWEN_TOOL_CALL_RE,
|
|
97
|
-
.gsub(LITERALS_RE,
|
|
98
|
-
.gsub(INDIVIDUAL_RE,
|
|
99
|
-
.gsub(PROMPT_LITERALS,
|
|
95
|
+
.gsub(QWEN_THINK_RE, "")
|
|
96
|
+
.gsub(GEMMA_THOUGHT_RE, "")
|
|
97
|
+
.gsub(QWEN_TOOL_CALL_RE, "")
|
|
98
|
+
.gsub(LITERALS_RE, "")
|
|
99
|
+
.gsub(INDIVIDUAL_RE, "")
|
|
100
|
+
.gsub(PROMPT_LITERALS, "")
|
|
100
101
|
# orphaned closing tags that may remain after block removal
|
|
101
|
-
.gsub(
|
|
102
|
-
.gsub(
|
|
102
|
+
.gsub(%r{</?think>}, "")
|
|
103
|
+
.gsub(%r{</?tool_call>}, "")
|
|
103
104
|
end
|
|
104
105
|
end
|
|
105
106
|
end
|
|
@@ -40,7 +40,8 @@ module Samagotchi
|
|
|
40
40
|
# @return [OwnerLock, nil] nil when another owner holds it
|
|
41
41
|
def self.acquire(session_dir, kind:, wait: DEFAULT_WAIT)
|
|
42
42
|
FileUtils.mkdir_p(session_dir)
|
|
43
|
-
|
|
43
|
+
# Held open on purpose: the flock lives as long as the OwnerLock (#release closes it).
|
|
44
|
+
file = File.open(path(session_dir), File::RDWR | File::CREAT, 0o644) # rubocop:disable Style/FileOpen
|
|
44
45
|
deadline = monotonic_now + wait.to_f
|
|
45
46
|
until file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
46
47
|
if monotonic_now >= deadline
|
|
@@ -51,6 +51,7 @@ module Samagotchi
|
|
|
51
51
|
# it runs as other commands do)
|
|
52
52
|
def command(name, description, anytime: false, &block)
|
|
53
53
|
raise ArgumentError, "command #{name.inspect} needs a block" unless block
|
|
54
|
+
|
|
54
55
|
name = name.to_s
|
|
55
56
|
raise ArgumentError, "command name #{name.inspect} must look like /name (a-z, 0-9, _ and -)" unless name.match?(COMMAND_NAME)
|
|
56
57
|
if (taken = @registries.commands.entries.find { |entry| entry.name == name })
|
|
@@ -78,9 +79,9 @@ module Samagotchi
|
|
|
78
79
|
# @param targets [#call, nil] args → what guardrail rules match: a Hash
|
|
79
80
|
# with paths: (absolute or relative to the cwd), command: (a shell
|
|
80
81
|
# command) and cwd:, each optional
|
|
81
|
-
def tool(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &
|
|
82
|
+
def tool(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &)
|
|
82
83
|
spec = Api.tool_spec(name, description, params: params, schema: schema, label: label, preview: preview,
|
|
83
|
-
targets: targets, &
|
|
84
|
+
targets: targets, &)
|
|
84
85
|
if (taken = @registries.tools[spec[:name]])
|
|
85
86
|
raise ArgumentError, "tool #{spec[:name]} is already registered (#{taken.source})"
|
|
86
87
|
end
|
|
@@ -124,9 +125,9 @@ module Samagotchi
|
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
# As Api#tool.
|
|
127
|
-
def tool(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &
|
|
128
|
+
def tool(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &)
|
|
128
129
|
spec = Api.tool_spec(name, description, params: params, schema: schema, label: label, preview: preview,
|
|
129
|
-
targets: targets, &
|
|
130
|
+
targets: targets, &)
|
|
130
131
|
raise ArgumentError, "tool #{spec[:name]} is registered twice" if @specs.any? { |s| s[:name] == spec[:name] }
|
|
131
132
|
|
|
132
133
|
@specs << spec
|
|
@@ -159,6 +160,7 @@ module Samagotchi
|
|
|
159
160
|
# @return [Service]
|
|
160
161
|
def service(name, eager: false, &block)
|
|
161
162
|
raise ArgumentError, "service #{name.inspect} needs a block" unless block
|
|
163
|
+
|
|
162
164
|
name = "#{@bundle}:#{name}"
|
|
163
165
|
raise ArgumentError, "service #{name} is registered twice" if @services.any? { |svc| svc.name == name }
|
|
164
166
|
raise ArgumentError, "this chi has no services (plugins: false)" unless @registries.services
|
|
@@ -249,6 +251,7 @@ module Samagotchi
|
|
|
249
251
|
# @return [Hash] {name:, schema:, label:, preview:, targets:, block:}
|
|
250
252
|
def self.tool_spec(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &block)
|
|
251
253
|
raise ArgumentError, "tool #{name.inspect} needs a block" unless block
|
|
254
|
+
|
|
252
255
|
name = name.to_s
|
|
253
256
|
raise ArgumentError, "tool name #{name.inspect} must be a-z, 0-9 and _ (at most 48)" unless name.match?(TOOL_NAME)
|
|
254
257
|
raise ArgumentError, "tool #{name}: preview must respond to #call" if preview && !preview.respond_to?(:call)
|
|
@@ -323,7 +326,7 @@ module Samagotchi
|
|
|
323
326
|
# (a call built without one, in a spec say: the call without its
|
|
324
327
|
# name), string keys, typed by +parameters+, frozen.
|
|
325
328
|
def self.args_of(call, parameters = nil)
|
|
326
|
-
given = call[:args].is_a?(Hash) ? call[:args] : call.reject { |key, _|
|
|
329
|
+
given = call[:args].is_a?(Hash) ? call[:args] : call.reject { |key, _| %i[name args].include?(key) }
|
|
327
330
|
Tools::Args.coerce(given, parameters).freeze
|
|
328
331
|
end
|
|
329
332
|
|
|
@@ -107,9 +107,7 @@ module Samagotchi
|
|
|
107
107
|
# $XDG_STATE_HOME/samagotchi/plugins/<bundle>/, created on first use.
|
|
108
108
|
# @return [String]
|
|
109
109
|
def data_dir
|
|
110
|
-
@data_dir ||=
|
|
111
|
-
File.join(Paths.state_dir(env: @env), "plugins", @bundle).tap { |dir| FileUtils.mkdir_p(dir) }
|
|
112
|
-
end
|
|
110
|
+
@data_dir ||= File.join(Paths.state_dir(env: @env), "plugins", @bundle).tap { |dir| FileUtils.mkdir_p(dir) }
|
|
113
111
|
end
|
|
114
112
|
|
|
115
113
|
# The conversation so far, a frozen copy, without the system prompt.
|