samagotchi 0.9.0 → 0.10.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 +56 -1
- data/docs/architecture.md +24 -9
- data/docs/cli.md +4 -3
- data/docs/guardrails.md +3 -2
- data/docs/hooks.md +44 -7
- data/docs/internals/gemma4-contract.md +1 -0
- data/docs/plugins.md +34 -12
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bootstrap_command.rb +29 -29
- data/lib/samagotchi/bridge/card_store.rb +4 -4
- data/lib/samagotchi/bridge/pending_card.rb +2 -2
- data/lib/samagotchi/bridge/sse_writer.rb +22 -28
- data/lib/samagotchi/bridge/turn_accumulator.rb +92 -5
- data/lib/samagotchi/bridge.rb +59 -112
- data/lib/samagotchi/bridge_client.rb +11 -28
- data/lib/samagotchi/bundle_command.rb +158 -212
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +3 -4
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +7 -3
- 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 +10 -10
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -4
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +4 -6
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +0 -1
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cli/command.rb +51 -0
- data/lib/samagotchi/cli/flags.rb +116 -0
- data/lib/samagotchi/config.rb +70 -55
- data/lib/samagotchi/config_text_edit.rb +107 -0
- data/lib/samagotchi/desktop_command.rb +24 -18
- data/lib/samagotchi/edit_preview.rb +2 -2
- data/lib/samagotchi/engine.rb +20 -75
- data/lib/samagotchi/events.rb +23 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +13 -30
- data/lib/samagotchi/hooks/loader.rb +23 -33
- data/lib/samagotchi/hooks/registry.rb +41 -0
- data/lib/samagotchi/hooks.rb +0 -3
- data/lib/samagotchi/host_registry.rb +15 -0
- data/lib/samagotchi/idle_reminders.rb +0 -7
- data/lib/samagotchi/image_store.rb +22 -0
- data/lib/samagotchi/kernel_loop.rb +12 -16
- data/lib/samagotchi/live_versions.rb +2 -13
- data/lib/samagotchi/llm/native_tool_normalizer.rb +10 -227
- data/lib/samagotchi/memory_bundle/builder.rb +1 -14
- data/lib/samagotchi/memory_bundle/index_updater.rb +2 -28
- data/lib/samagotchi/memory_bundle/installer.rb +1 -27
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -6
- data/lib/samagotchi/memory_bundle/status.rb +1 -14
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +1 -24
- data/lib/samagotchi/memory_paths.rb +10 -0
- data/lib/samagotchi/note_command.rb +23 -37
- data/lib/samagotchi/owner_lock.rb +14 -3
- data/lib/samagotchi/plugin/api.rb +11 -17
- data/lib/samagotchi/plugin/context.rb +70 -15
- data/lib/samagotchi/plugin/loader.rb +2 -3
- data/lib/samagotchi/sampling_settings.rb +3 -5
- data/lib/samagotchi/self_report.rb +6 -9
- data/lib/samagotchi/send_command.rb +33 -52
- data/lib/samagotchi/session.rb +88 -92
- data/lib/samagotchi/session_archive_command.rb +8 -14
- data/lib/samagotchi/session_commands.rb +3 -21
- data/lib/samagotchi/session_delete_command.rb +15 -29
- data/lib/samagotchi/session_manager.rb +27 -13
- data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -80
- data/lib/samagotchi/terminal_ui/attached_view.rb +1 -10
- data/lib/samagotchi/terminal_ui/between_turns.rb +133 -0
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +51 -1
- data/lib/samagotchi/terminal_ui/status_row.rb +32 -0
- data/lib/samagotchi/terminal_ui/thinking_line.rb +3 -3
- data/lib/samagotchi/terminal_ui.rb +156 -234
- data/lib/samagotchi/thinking.rb +2 -3
- data/lib/samagotchi/thought_stream_splitter.rb +10 -9
- data/lib/samagotchi/tool_call_parser.rb +56 -292
- data/lib/samagotchi/tools/builtin_calls.rb +146 -0
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/edit.rb +21 -29
- data/lib/samagotchi/tools/memory.rb +1 -11
- data/lib/samagotchi/tools/send_note.rb +1 -1
- data/lib/samagotchi/update_command.rb +32 -22
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -5
- data/lib/samagotchi/web/app.rb +89 -151
- data/lib/samagotchi/web/public/activity.js +1 -11
- data/lib/samagotchi/web/public/annotate_bar.js +120 -0
- data/lib/samagotchi/web/public/app.js +158 -785
- data/lib/samagotchi/web/public/chunk_router.js +7 -6
- data/lib/samagotchi/web/public/command_list.js +99 -0
- data/lib/samagotchi/web/public/data.js +22 -66
- data/lib/samagotchi/web/public/format.js +0 -23
- data/lib/samagotchi/web/public/index.html +0 -2
- data/lib/samagotchi/web/public/model_pick.js +0 -7
- data/lib/samagotchi/web/public/notifications.js +169 -0
- data/lib/samagotchi/web/public/question_cards.js +352 -0
- data/lib/samagotchi/web/public/session_state.js +34 -0
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/turn_events.js +11 -2
- data/lib/samagotchi/web/public/turn_model.js +0 -14
- data/lib/samagotchi/web/session_hub.rb +6 -4
- data/lib/samagotchi/web/session_summary.rb +5 -3
- data/lib/samagotchi/worker.rb +59 -53
- data/lib/samagotchi/worker_sidecar.rb +27 -1
- metadata +12 -1
|
@@ -17,7 +17,8 @@ module Samagotchi
|
|
|
17
17
|
# exactly what the Bridge's minimal server expects.
|
|
18
18
|
class BridgeClient
|
|
19
19
|
HOST = "127.0.0.1"
|
|
20
|
-
|
|
20
|
+
# A reply's status line; its code is group 1.
|
|
21
|
+
STATUS_LINE = %r{\AHTTP/1\.[01] (\d{3})}
|
|
21
22
|
STREAM_CONNECT_ATTEMPTS = 3
|
|
22
23
|
# Seconds #stream waits for bytes before it asks `running` again.
|
|
23
24
|
STREAM_POLL = 0.5
|
|
@@ -59,24 +60,7 @@ module Samagotchi
|
|
|
59
60
|
# @param session_dir [String]
|
|
60
61
|
# @return [Integer, nil]
|
|
61
62
|
def self.sidecar_port(session_dir, host: HOST)
|
|
62
|
-
|
|
63
|
-
return nil unless port&.positive?
|
|
64
|
-
|
|
65
|
-
# Validate liveness: stale sidecar after worker death causes ECONNREFUSED
|
|
66
|
-
# which surfaces as WEBrick ERROR. Probe quickly and clean up if dead.
|
|
67
|
-
begin
|
|
68
|
-
Socket.tcp(host, port, connect_timeout: PROBE_TIMEOUT).close
|
|
69
|
-
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError, IOError, StandardError
|
|
70
|
-
begin
|
|
71
|
-
File.unlink(WorkerSidecar.path(session_dir))
|
|
72
|
-
rescue StandardError
|
|
73
|
-
nil
|
|
74
|
-
end
|
|
75
|
-
return nil
|
|
76
|
-
end
|
|
77
|
-
port
|
|
78
|
-
rescue StandardError
|
|
79
|
-
nil
|
|
63
|
+
WorkerSidecar.live_port(session_dir, unlink: true, host: host)
|
|
80
64
|
end
|
|
81
65
|
|
|
82
66
|
# @return [BridgeClient, nil] a client for the live Bridge in +session_dir+
|
|
@@ -184,10 +168,11 @@ module Samagotchi
|
|
|
184
168
|
post("recap", {}, read_body: true)
|
|
185
169
|
end
|
|
186
170
|
|
|
187
|
-
# POST /session/:id/cancel. 202 = requested
|
|
188
|
-
#
|
|
171
|
+
# POST /session/:id/cancel. 202 = requested (body names the reason the
|
|
172
|
+
# Bridge took), 409 = no active turn.
|
|
173
|
+
# @return [Response]
|
|
189
174
|
def cancel(reason:)
|
|
190
|
-
post("cancel", { reason: reason }, read_body:
|
|
175
|
+
post("cancel", { reason: reason }, read_body: true)
|
|
191
176
|
end
|
|
192
177
|
|
|
193
178
|
# GET /session/:id/<path> as JSON.
|
|
@@ -197,10 +182,8 @@ module Samagotchi
|
|
|
197
182
|
sock.write("GET /session/#{@session_id}/#{path} HTTP/1.1\r\nHost: #{@host}:#{@port}\r\nConnection: close\r\n\r\n")
|
|
198
183
|
response = read_reply(sock, path)
|
|
199
184
|
sock.close rescue nil
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
status_line = response.lines.first.to_s
|
|
203
|
-
return nil unless status_line.include?("200")
|
|
185
|
+
# A 500 whose text has "200" in it is not a 200.
|
|
186
|
+
return nil unless response && response[STATUS_LINE, 1] == "200"
|
|
204
187
|
|
|
205
188
|
body = response.split("\r\n\r\n", 2)[1] || ""
|
|
206
189
|
JSON.parse(body)
|
|
@@ -278,7 +261,7 @@ module Samagotchi
|
|
|
278
261
|
sock.write("GET /session/#{@session_id}/stream#{query} HTTP/1.1\r\nHost: #{@host}:#{@port}\r\nAccept: text/event-stream\r\n#{last_event_line}Connection: keep-alive\r\n\r\n")
|
|
279
262
|
raise Errno::ETIMEDOUT if timeout && !sock.wait_readable(timeout)
|
|
280
263
|
|
|
281
|
-
status = sock.gets.to_s[
|
|
264
|
+
status = sock.gets.to_s[STATUS_LINE, 1].to_i
|
|
282
265
|
while (line = sock.gets)
|
|
283
266
|
break if line.strip.empty?
|
|
284
267
|
end
|
|
@@ -301,7 +284,7 @@ module Samagotchi
|
|
|
301
284
|
sock.write("POST /session/#{@session_id}/#{path} HTTP/1.1\r\nHost: #{@host}:#{@port}\r\nContent-Type: application/json\r\nContent-Length: #{json_body.bytesize}\r\nConnection: close\r\n\r\n#{json_body}")
|
|
302
285
|
reply = read_reply(sock, path, whole: read_body)
|
|
303
286
|
body = read_body ? reply.split("\r\n\r\n", 2)[1] : nil
|
|
304
|
-
Response.new(status: reply[
|
|
287
|
+
Response.new(status: reply[STATUS_LINE, 1].to_i, body: body)
|
|
305
288
|
ensure
|
|
306
289
|
sock&.close rescue nil
|
|
307
290
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cli/flags"
|
|
3
4
|
require_relative "memory_bundle"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
@@ -49,6 +50,38 @@ module Samagotchi
|
|
|
49
50
|
TEXT
|
|
50
51
|
UPGRADE_HELP = "Usage: chi bundle upgrade <source> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]"
|
|
51
52
|
UNINSTALL_HELP = "Usage: chi bundle uninstall <bundle> [--scope system|project] [--force]"
|
|
53
|
+
BUILD_HELP = <<~TEXT
|
|
54
|
+
Usage: chi bundle build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
|
|
55
|
+
|
|
56
|
+
build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
|
|
57
|
+
Build local memories and installed hooks into a shareable bundle (dir or zip).
|
|
58
|
+
--scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
|
|
59
|
+
FILES... optional allowlist of *.md basenames to include (default: all).
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
chi bundle build --scope system
|
|
63
|
+
chi bundle build --scope project --name my-bundle --version 1.0.0 --out bundle.zip
|
|
64
|
+
chi bundle build --scope system --out ./my-bundle/ identity.md work.md
|
|
65
|
+
TEXT
|
|
66
|
+
|
|
67
|
+
# The subcommands' flags: --help only; a word not starting with -- is
|
|
68
|
+
# a positional (install's source, -h included). --scope takes whatever
|
|
69
|
+
# follows it; build's value flags not a --flag.
|
|
70
|
+
INSTALL_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
71
|
+
f.value "--scope"
|
|
72
|
+
f.switch "--force"
|
|
73
|
+
end
|
|
74
|
+
UNINSTALL_FLAGS = INSTALL_FLAGS
|
|
75
|
+
UPGRADE_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
76
|
+
f.value "--scope"
|
|
77
|
+
f.switch "--force"
|
|
78
|
+
f.switch "--dry-run"
|
|
79
|
+
f.switch "--agent"
|
|
80
|
+
f.switch "--no-agent", key: :agent, set: false
|
|
81
|
+
end
|
|
82
|
+
BUILD_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/, dash_values: false) do |f|
|
|
83
|
+
%w[--scope --name --version --description --out].each { |name| f.value name }
|
|
84
|
+
end
|
|
52
85
|
|
|
53
86
|
# @param argv [Array<String>] the arguments after "bundle"
|
|
54
87
|
def initialize(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
|
|
@@ -76,174 +109,157 @@ module Samagotchi
|
|
|
76
109
|
when "build" then build(@argv[1..])
|
|
77
110
|
else
|
|
78
111
|
@stderr.puts "Unknown bundle subcommand: #{sub}. Use: install, upgrade, uninstall, status, diff, list, build"
|
|
79
|
-
return
|
|
112
|
+
return 2
|
|
80
113
|
end
|
|
81
114
|
end
|
|
82
115
|
|
|
83
116
|
private
|
|
84
117
|
|
|
85
118
|
def install(rest)
|
|
86
|
-
parsed = parse_flags(rest, "install", help: INSTALL_HELP
|
|
119
|
+
parsed = parse_flags(INSTALL_FLAGS, rest, "install", help: INSTALL_HELP)
|
|
87
120
|
return parsed if parsed.is_a?(Integer)
|
|
88
121
|
|
|
89
|
-
opts, source = parsed
|
|
122
|
+
opts, source = parsed.options, parsed.args.last
|
|
90
123
|
scope = opts[:scope]
|
|
91
124
|
force = opts.fetch(:force, false)
|
|
92
125
|
if source.nil? || source.empty?
|
|
93
126
|
@stderr.puts "Usage: chi bundle install <source> [--scope system|project] [--force]"
|
|
94
|
-
return
|
|
127
|
+
return 2
|
|
95
128
|
end
|
|
96
129
|
expanded_source = expand_source(source)
|
|
97
130
|
if Samagotchi::MemoryBundle::Profile.shipped_meta?(expanded_source)
|
|
98
131
|
return install_profile(expanded_source, scope: scope, force: force, dry_run: false, word: "Install")
|
|
99
132
|
end
|
|
100
|
-
bundle_name =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return 1
|
|
105
|
-
end
|
|
106
|
-
installer = Samagotchi::MemoryBundle::Installer.new(
|
|
107
|
-
source: expanded_source,
|
|
108
|
-
name: bundle_name,
|
|
109
|
-
scope: scope,
|
|
110
|
-
force: force,
|
|
111
|
-
strict: true
|
|
112
|
-
)
|
|
113
|
-
begin
|
|
114
|
-
_nd, manifest = installer.run
|
|
115
|
-
@stdout.puts installer.summary
|
|
116
|
-
if manifest && manifest.hooks && !manifest.hooks.empty?
|
|
117
|
-
hook_cnt = manifest.hooks.size
|
|
118
|
-
@stdout.puts "Hooks: #{hook_cnt} hook(s) (#{manifest.hooks.keys.join(', ')})"
|
|
119
|
-
end
|
|
120
|
-
@stdout.puts "Plugin: #{manifest.plugin[:file]} (loads at the next chi start)" if manifest&.plugin
|
|
121
|
-
@stdout.puts "Provenance written to: #{Samagotchi::MemoryBundle::Provenance.bundles_dir}/#{bundle_name}/" if manifest
|
|
122
|
-
return 0
|
|
123
|
-
rescue Samagotchi::MemoryBundle::Installer::InstallError => e
|
|
124
|
-
@stderr.puts "Install failed: #{e.message}"
|
|
125
|
-
return 1
|
|
133
|
+
bundle_name = bundle_name_or_fail(expanded_source, "Install") or return 1
|
|
134
|
+
run_installer(installer_for(expanded_source, bundle_name, scope: scope, force: force), failed: "Install") do |manifest|
|
|
135
|
+
print_hooks_and_plugin(manifest)
|
|
136
|
+
print_provenance(bundle_name, manifest)
|
|
126
137
|
end
|
|
127
138
|
end
|
|
128
139
|
|
|
129
140
|
def upgrade(rest)
|
|
130
|
-
parsed = parse_flags(rest, "upgrade", help: UPGRADE_HELP
|
|
131
|
-
bools: { "--force" => [:force, true], "--dry-run" => [:dry_run, true],
|
|
132
|
-
"--agent" => [:agent, true], "--no-agent" => [:agent, false] })
|
|
141
|
+
parsed = parse_flags(UPGRADE_FLAGS, rest, "upgrade", help: UPGRADE_HELP)
|
|
133
142
|
return parsed if parsed.is_a?(Integer)
|
|
134
143
|
|
|
135
|
-
opts, source = parsed
|
|
144
|
+
opts, source = parsed.options, parsed.args.last
|
|
136
145
|
scope = opts[:scope]
|
|
137
146
|
force = opts.fetch(:force, false)
|
|
138
147
|
dry_run = opts.fetch(:dry_run, false)
|
|
139
148
|
agent = opts[:agent]
|
|
140
149
|
if source.nil? || source.empty?
|
|
141
150
|
@stderr.puts "Usage: chi bundle upgrade <source> [--scope system|project] [--force] [--dry-run]"
|
|
142
|
-
return
|
|
151
|
+
return 2
|
|
143
152
|
end
|
|
144
153
|
expanded_source = expand_source(source)
|
|
145
154
|
if Samagotchi::MemoryBundle::Profile.shipped_meta?(expanded_source)
|
|
146
155
|
return install_profile(expanded_source, scope: scope, force: force, dry_run: dry_run, word: "Upgrade")
|
|
147
156
|
end
|
|
148
|
-
bundle_name =
|
|
149
|
-
|
|
150
|
-
rescue Samagotchi::MemoryBundle::SourceNormalizer::UnknownSourceError => e
|
|
151
|
-
@stderr.puts "Upgrade failed: #{e.message}"
|
|
152
|
-
return 1
|
|
153
|
-
end
|
|
154
|
-
provenance = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name)
|
|
155
|
-
unless provenance.installed?
|
|
157
|
+
bundle_name = bundle_name_or_fail(expanded_source, "Upgrade") or return 1
|
|
158
|
+
unless Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).installed?
|
|
156
159
|
@stderr.puts "Bundle '#{bundle_name}' not installed — falling back to install"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
@stdout.puts "(dry-run: no changes written)"
|
|
164
|
-
return 0
|
|
165
|
-
end
|
|
166
|
-
@stdout.puts "Provenance written to: #{Samagotchi::MemoryBundle::Provenance.bundles_dir}/#{bundle_name}/" if manifest
|
|
167
|
-
return 0
|
|
168
|
-
rescue Samagotchi::MemoryBundle::Installer::InstallError => e
|
|
169
|
-
@stderr.puts "Install failed: #{e.message}"; return 1
|
|
160
|
+
# (no Hooks/Plugin lines here)
|
|
161
|
+
installer = installer_for(expanded_source, bundle_name, scope: scope, force: force, dry_run: dry_run)
|
|
162
|
+
return run_installer(installer, failed: "Install") do |manifest|
|
|
163
|
+
next dry_run_done if dry_run
|
|
164
|
+
|
|
165
|
+
print_provenance(bundle_name, manifest)
|
|
170
166
|
end
|
|
171
167
|
end
|
|
172
|
-
installer =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
168
|
+
installer = installer_for(expanded_source, bundle_name, scope: scope, force: force, upgrade: true, dry_run: dry_run)
|
|
169
|
+
run_installer(installer, failed: "Upgrade") do |manifest|
|
|
170
|
+
print_hooks_and_plugin(manifest, plugin: !dry_run)
|
|
171
|
+
next dry_run_done if dry_run
|
|
172
|
+
next upgrade_conflicts(installer, bundle_name, expanded_source, agent) if installer.conflicts.any? && !force
|
|
173
|
+
|
|
174
|
+
print_provenance(bundle_name, manifest)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# The bundle's name, or nil after "<word> failed: …".
|
|
179
|
+
def bundle_name_or_fail(expanded_source, word)
|
|
180
|
+
bundle_name_for(expanded_source)
|
|
181
|
+
rescue Samagotchi::MemoryBundle::SourceNormalizer::UnknownSourceError => e
|
|
182
|
+
@stderr.puts "#{word} failed: #{e.message}"
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def installer_for(source, name, scope:, force:, **options)
|
|
187
|
+
Samagotchi::MemoryBundle::Installer.new(source: source, name: name, scope: scope, force: force, strict: true, **options)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Runs +installer+, prints its summary, then yields the manifest.
|
|
191
|
+
# @return [Integer] the block's exit status, or 1 after "<failed> failed: …"
|
|
192
|
+
def run_installer(installer, failed:)
|
|
193
|
+
_nd, manifest = installer.run
|
|
194
|
+
@stdout.puts installer.summary
|
|
195
|
+
yield manifest
|
|
196
|
+
rescue Samagotchi::MemoryBundle::Installer::InstallError => e
|
|
197
|
+
@stderr.puts "#{failed} failed: #{e.message}"
|
|
198
|
+
1
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def print_hooks_and_plugin(manifest, plugin: true)
|
|
202
|
+
@stdout.puts "Hooks: #{manifest.hooks.size} hook(s) (#{manifest.hooks.keys.join(', ')})" if manifest&.hooks&.any?
|
|
203
|
+
@stdout.puts "Plugin: #{manifest.plugin[:file]} (loads at the next chi start)" if plugin && manifest&.plugin
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @return [Integer] 0
|
|
207
|
+
def print_provenance(bundle_name, manifest)
|
|
208
|
+
@stdout.puts "Provenance written to: #{Samagotchi::MemoryBundle::Provenance.bundles_dir}/#{bundle_name}/" if manifest
|
|
209
|
+
0
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @return [Integer] 0
|
|
213
|
+
def dry_run_done
|
|
214
|
+
@stdout.puts "(dry-run: no changes written)"
|
|
215
|
+
0
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# An upgrade that kept edited files: launch an agent to merge them
|
|
219
|
+
# (--agent, or yes on a terminal), or say how to take the bundle's.
|
|
220
|
+
# @return [Integer] 0 resolved, 2 kept the edits
|
|
221
|
+
def upgrade_conflicts(installer, bundle_name, expanded_source, agent)
|
|
222
|
+
@stdout.puts "\n#{installer.conflicts.size} conflict(s) need resolution."
|
|
223
|
+
installer.conflicts.each { |k, _| @stdout.puts " conflict: #{k}" }
|
|
224
|
+
launch = if agent.nil? && @stdin.tty?
|
|
225
|
+
@stdout.print "Conflicts detected — launch interactive agent to resolve? [y/N] "
|
|
226
|
+
ans = begin; @stdin.gets; rescue => _e; nil; end
|
|
227
|
+
ans && ans.strip.downcase.start_with?("y")
|
|
228
|
+
elsif agent.nil?
|
|
229
|
+
@stdout.puts "Non-interactive terminal: kept your edits in the file(s) above; the rest is upgraded. Re-run with --force to take the bundle's version, or --agent in a TTY to merge."
|
|
230
|
+
return 2
|
|
231
|
+
else
|
|
232
|
+
agent
|
|
233
|
+
end
|
|
234
|
+
unless launch
|
|
235
|
+
@stdout.puts "Kept your edits in the file(s) above; the rest is upgraded. chi bundle diff #{bundle_name} FILE shows the base; re-run with --force to take the bundle's version."
|
|
236
|
+
return 2
|
|
230
237
|
end
|
|
238
|
+
|
|
239
|
+
prompt = build_conflict_prompt(bundle_name, installer.conflicts, expanded_source)
|
|
240
|
+
@stdout.puts "Launching interactive session for conflict resolution… (/exit when done)"
|
|
241
|
+
require "samagotchi/terminal_ui"
|
|
242
|
+
Samagotchi::TerminalUI.new(prompt: prompt).run
|
|
243
|
+
# The installer already recorded the upgrade (conflicted files kept
|
|
244
|
+
# their old base); the resolved files now start from the bundle's
|
|
245
|
+
# version.
|
|
246
|
+
Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(installer.conflicts)
|
|
247
|
+
@stdout.puts "Provenance updated after interactive resolution."
|
|
248
|
+
@stdout.puts "Upgrade resolved interactively."
|
|
249
|
+
0
|
|
231
250
|
end
|
|
232
251
|
|
|
233
252
|
def uninstall(rest)
|
|
234
|
-
parsed = parse_flags(rest, "uninstall", help: UNINSTALL_HELP
|
|
253
|
+
parsed = parse_flags(UNINSTALL_FLAGS, rest, "uninstall", help: UNINSTALL_HELP)
|
|
235
254
|
return parsed if parsed.is_a?(Integer)
|
|
236
255
|
|
|
237
|
-
opts, bundle_name = parsed
|
|
256
|
+
opts, bundle_name = parsed.options, parsed.args.last
|
|
238
257
|
scope = opts[:scope]
|
|
239
258
|
force = opts.fetch(:force, false)
|
|
240
259
|
if bundle_name.nil? || bundle_name.empty?
|
|
241
260
|
@stderr.puts "Usage: chi bundle uninstall <bundle> [--scope system|project] [--force]"
|
|
242
|
-
return
|
|
261
|
+
return 2
|
|
243
262
|
end
|
|
244
|
-
# Propagate overrides
|
|
245
|
-
Samagotchi::MemoryBundle::Uninstaller.system_dir_override = Samagotchi::MemoryBundle::Installer.system_dir_override
|
|
246
|
-
Samagotchi::MemoryBundle::Uninstaller.project_dir_base_override = Samagotchi::MemoryBundle::Installer.project_dir_base_override
|
|
247
263
|
data = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).read
|
|
248
264
|
return uninstall_profile(bundle_name, force: force) if Samagotchi::MemoryBundle::Profile.installed_meta?(bundle_name, data)
|
|
249
265
|
|
|
@@ -377,7 +393,7 @@ module Samagotchi
|
|
|
377
393
|
bname = args[0]
|
|
378
394
|
file_arg = args[1]
|
|
379
395
|
if bname.nil? || bname.empty?
|
|
380
|
-
@stderr.puts "Usage: chi bundle diff <bundle> [file]"; return
|
|
396
|
+
@stderr.puts "Usage: chi bundle diff <bundle> [file]"; return 2
|
|
381
397
|
end
|
|
382
398
|
prov = Samagotchi::MemoryBundle::Provenance.new(name: bname)
|
|
383
399
|
data = prov.read
|
|
@@ -509,65 +525,17 @@ module Samagotchi
|
|
|
509
525
|
end
|
|
510
526
|
|
|
511
527
|
def build(rest)
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
filter_files =
|
|
518
|
-
i = 0
|
|
519
|
-
while i < rest.size
|
|
520
|
-
arg = rest[i]
|
|
521
|
-
if arg.start_with?("--")
|
|
522
|
-
nxt = rest[i + 1]
|
|
523
|
-
if %w[-h --help help].include?(arg)
|
|
524
|
-
@stdout.puts "Usage: chi bundle build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]"
|
|
525
|
-
@stdout.puts ""
|
|
526
|
-
@stdout.puts " build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]"
|
|
527
|
-
@stdout.puts " Build local memories and installed hooks into a shareable bundle (dir or zip)."
|
|
528
|
-
@stdout.puts " --scope selects source dir (default: system). --out inferred from extension; default <name>.zip."
|
|
529
|
-
@stdout.puts " FILES... optional allowlist of *.md basenames to include (default: all)."
|
|
530
|
-
@stdout.puts ""
|
|
531
|
-
@stdout.puts " Examples:"
|
|
532
|
-
@stdout.puts " chi bundle build --scope system"
|
|
533
|
-
@stdout.puts " chi bundle build --scope project --name my-bundle --version 1.0.0 --out bundle.zip"
|
|
534
|
-
@stdout.puts " chi bundle build --scope system --out ./my-bundle/ identity.md work.md"
|
|
535
|
-
return 0
|
|
536
|
-
elsif arg == "--scope" && nxt && !nxt.start_with?("--")
|
|
537
|
-
scope = nxt; i += 2
|
|
538
|
-
elsif arg.start_with?("--scope=")
|
|
539
|
-
scope = arg.split("=", 2).last; i += 1
|
|
540
|
-
elsif arg == "--name" && nxt && !nxt.start_with?("--")
|
|
541
|
-
name = nxt; i += 2
|
|
542
|
-
elsif arg.start_with?("--name=")
|
|
543
|
-
name = arg.split("=", 2).last; i += 1
|
|
544
|
-
elsif arg == "--version" && nxt && !nxt.start_with?("--")
|
|
545
|
-
version = nxt; i += 2
|
|
546
|
-
elsif arg.start_with?("--version=")
|
|
547
|
-
version = arg.split("=", 2).last; i += 1
|
|
548
|
-
elsif arg == "--description" && nxt && !nxt.start_with?("--")
|
|
549
|
-
description = nxt; i += 2
|
|
550
|
-
elsif arg.start_with?("--description=")
|
|
551
|
-
description = arg.split("=", 2).last; i += 1
|
|
552
|
-
elsif arg == "--out" && nxt && !nxt.start_with?("--")
|
|
553
|
-
out = nxt; i += 2
|
|
554
|
-
elsif arg.start_with?("--out=")
|
|
555
|
-
out = arg.split("=", 2).last; i += 1
|
|
556
|
-
else
|
|
557
|
-
@stderr.puts "Unknown bundle build flag: #{arg}"
|
|
558
|
-
return 1
|
|
559
|
-
end
|
|
560
|
-
else
|
|
561
|
-
filter_files << arg
|
|
562
|
-
i += 1
|
|
563
|
-
end
|
|
564
|
-
end
|
|
528
|
+
parsed = parse_flags(BUILD_FLAGS, rest, "build", help: BUILD_HELP)
|
|
529
|
+
return parsed if parsed.is_a?(Integer)
|
|
530
|
+
|
|
531
|
+
scope, name, version, out = parsed.options.values_at(:scope, :name, :version, :out)
|
|
532
|
+
description = parsed.options.fetch(:description, "")
|
|
533
|
+
filter_files = parsed.args.empty? ? nil : parsed.args
|
|
565
534
|
# Validate scope if given
|
|
566
535
|
if scope && !%w[system project].include?(scope.to_s.strip.downcase)
|
|
567
536
|
@stderr.puts "Invalid scope '#{scope}', expected system or project"
|
|
568
|
-
return
|
|
537
|
+
return 2
|
|
569
538
|
end
|
|
570
|
-
filter_files = nil if filter_files.empty?
|
|
571
539
|
begin
|
|
572
540
|
builder = Samagotchi::MemoryBundle::Builder.new(
|
|
573
541
|
scope: scope,
|
|
@@ -592,44 +560,22 @@ module Samagotchi
|
|
|
592
560
|
end
|
|
593
561
|
end
|
|
594
562
|
|
|
595
|
-
#
|
|
596
|
-
# first --help or unknown --flag: --scope V (whatever V is) or
|
|
597
|
-
# --scope=V, and the +bools+ ("--flag" => [key, value]). Anything not
|
|
598
|
-
# starting with -- is the positional; the last one wins.
|
|
563
|
+
# +rest+ through +flags+, stopping at the first --help or error.
|
|
599
564
|
#
|
|
600
|
-
# @return [
|
|
601
|
-
#
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
return 0
|
|
613
|
-
elsif arg == "--scope" && nxt
|
|
614
|
-
opts[:scope] = nxt
|
|
615
|
-
i += 2
|
|
616
|
-
elsif arg.start_with?("--scope=")
|
|
617
|
-
opts[:scope] = arg.split("=", 2).last
|
|
618
|
-
i += 1
|
|
619
|
-
elsif bools.key?(arg)
|
|
620
|
-
key, value = bools[arg]
|
|
621
|
-
opts[key] = value
|
|
622
|
-
i += 1
|
|
623
|
-
else
|
|
624
|
-
@stderr.puts "Unknown bundle #{sub} flag: #{arg}"
|
|
625
|
-
return 1
|
|
626
|
-
end
|
|
627
|
-
else
|
|
628
|
-
positional = arg
|
|
629
|
-
i += 1
|
|
630
|
-
end
|
|
565
|
+
# @return [CLI::Flags::Result, Integer] the result, or the exit status
|
|
566
|
+
# after the help or an unknown flag (a value flag without its value
|
|
567
|
+
# reads as one)
|
|
568
|
+
def parse_flags(flags, rest, sub, help:)
|
|
569
|
+
parsed = flags.parse(rest)
|
|
570
|
+
if parsed.help
|
|
571
|
+
@stdout.puts help
|
|
572
|
+
return 0
|
|
573
|
+
end
|
|
574
|
+
if parsed.error
|
|
575
|
+
@stderr.puts "Unknown bundle #{sub} flag: #{parsed.error.arg}"
|
|
576
|
+
return 2
|
|
631
577
|
end
|
|
632
|
-
|
|
578
|
+
parsed
|
|
633
579
|
end
|
|
634
580
|
|
|
635
581
|
# helpers for bundle_name derivation
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: check-in
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.3
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Checks on a long turn: after N tool calls with no answer, a card asks the user (Nudge / Keep going / Stop), or the model is nudged to say what it has found"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:c5aa4c49989723ec2370184cee36f4c8515d9df818b2c45422a5efb6c4ba31a7
|
|
10
10
|
requires_chi: ">= 0.3.0"
|
|
@@ -32,7 +32,6 @@ class Plugin
|
|
|
32
32
|
USAGE = "usage: /checkin [on|off|<calls>|mode ask|nudge|notify|nudge|later|stop]"
|
|
33
33
|
|
|
34
34
|
def initialize(settings = {})
|
|
35
|
-
settings = {} unless settings.is_a?(Hash)
|
|
36
35
|
@after = positive(settings["after"]) || 50
|
|
37
36
|
@every = positive(settings["every"]) || @after
|
|
38
37
|
@mode = MODES.include?(settings["mode"].to_s) ? settings["mode"].to_s : "ask"
|
|
@@ -85,13 +84,13 @@ class Plugin
|
|
|
85
84
|
@next_at = @count + @every
|
|
86
85
|
[@count, @mode]
|
|
87
86
|
end
|
|
88
|
-
check_in(
|
|
87
|
+
check_in(ctx, *due) if due
|
|
89
88
|
end
|
|
90
89
|
|
|
91
|
-
def check_in(
|
|
90
|
+
def check_in(ctx, count, mode)
|
|
92
91
|
case mode
|
|
93
92
|
when "nudge"
|
|
94
|
-
ctx.notify("nudged the model after #{count} tool calls") if
|
|
93
|
+
ctx.notify("nudged the model after #{count} tool calls") if ctx.steer(message(count))
|
|
95
94
|
when "notify"
|
|
96
95
|
ctx.notify("#{count} tool calls in this turn, no answer yet")
|
|
97
96
|
else
|
|
@@ -25,7 +25,6 @@ class KnownNames
|
|
|
25
25
|
ASK_OPTIONS = ["Correct it and run", "Run as is", "Deny"].freeze
|
|
26
26
|
|
|
27
27
|
def initialize(settings = {})
|
|
28
|
-
settings = {} unless settings.is_a?(Hash)
|
|
29
28
|
@names = list(settings["names"])
|
|
30
29
|
@ignore = list(settings["ignore"]).map(&:downcase)
|
|
31
30
|
@mode = MODES.include?(settings["mode"].to_s) ? settings["mode"].to_s : "reject"
|
|
@@ -175,13 +174,18 @@ class KnownNames
|
|
|
175
174
|
event[:notify]&.call("rejected #{tool}: \"#{hit[:miss]}\" looks like \"#{hit[:name]}\"")
|
|
176
175
|
end
|
|
177
176
|
|
|
177
|
+
# Tools whose content is file text: it is left as written.
|
|
178
|
+
FILE_TEXT_TOOLS = %w[write memory_write].freeze
|
|
179
|
+
|
|
178
180
|
# The same call with the near miss replaced (whole tokens, everywhere it
|
|
179
|
-
# appears in the content, path and cwd).
|
|
181
|
+
# appears in the command or read path (content), the path and the cwd).
|
|
182
|
+
# Not in file text: a write's content, an edit's old and new text.
|
|
180
183
|
def correct(event, hit)
|
|
181
184
|
call = event[:call]
|
|
182
185
|
pattern = /(?<![A-Za-z0-9])#{Regexp.escape(hit[:miss])}(?![A-Za-z0-9])/
|
|
183
186
|
fixed = call.dup
|
|
184
|
-
%i[content path cwd]
|
|
187
|
+
keys = FILE_TEXT_TOOLS.include?(call[:name].to_s) ? %i[path cwd] : %i[content path cwd]
|
|
188
|
+
keys.each do |key|
|
|
185
189
|
next unless fixed[key].is_a?(String)
|
|
186
190
|
|
|
187
191
|
fixed[key] = fixed[key].gsub(pattern, hit[:name])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: known-names
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.2
|
|
4
4
|
scope: system
|
|
5
5
|
description: Catches a near-miss of a protected name (home folder, login, git name, repo name, names from config) in a tool call and rejects it with the right spelling; mode ask / correct optional
|
|
6
6
|
trust_level: reviewed
|
|
@@ -8,7 +8,7 @@ files:
|
|
|
8
8
|
known_names.md: sha256:d5348b95b6c655017b541910b36e384220da280818160201f6ec9a50e1e4287c
|
|
9
9
|
hooks:
|
|
10
10
|
known_names.rb:
|
|
11
|
-
sha256: sha256:
|
|
11
|
+
sha256: sha256:f62bc72a64bb8b36d03374b0e9152a0492cc066e04f76129ae50d8cda9467f15
|
|
12
12
|
event: before_tool_call
|
|
13
13
|
on_error: log
|
|
14
14
|
priority: 50
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loop-guard
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.2
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Breaks loops: the same tool call with the same result again and again in one turn is denied with advice (after a few denies the turn stops with a card), and thinking that repeats itself while it streams is cut and the model asked again"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:e62c68d1a2280adf55b150488d0d85bbbc35ffce54dfed387a86adceb85e4d34
|
|
10
10
|
requires_chi: ">= 0.6.0"
|