samagotchi 0.3.0 → 0.5.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 +162 -1
- data/README.md +29 -2
- data/bin/chi +60 -69
- data/docs/cli.md +211 -77
- data/docs/configuration.md +118 -21
- data/docs/desktop.md +39 -4
- data/docs/guardrails.md +11 -0
- data/docs/hooks.md +89 -7
- data/docs/memory.md +40 -0
- data/docs/plugins.md +50 -0
- data/docs/releasing.md +15 -12
- data/docs/sessions.md +20 -18
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
- data/lib/samagotchi/bridge.rb +20 -12
- data/lib/samagotchi/bundles/skills/manifest.yml +10 -0
- data/lib/samagotchi/bundles/skills/plugin.rb +419 -0
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +178 -5
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +9 -10
- data/lib/samagotchi/bundles/system/delegated.md +6 -7
- data/lib/samagotchi/bundles/system/identity.md +5 -0
- data/lib/samagotchi/bundles/system/manifest.yml +6 -6
- data/lib/samagotchi/bundles/system/memory_guide.md +26 -0
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/client.rb +25 -26
- data/lib/samagotchi/commands/registry.rb +8 -0
- data/lib/samagotchi/config.rb +97 -113
- data/lib/samagotchi/desktop/macos/App.swift +12 -8
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +4 -2
- data/lib/samagotchi/desktop/macos/Images.swift +113 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +112 -9
- data/lib/samagotchi/desktop/macos.rb +59 -8
- data/lib/samagotchi/desktop_command.rb +6 -3
- data/lib/samagotchi/edit_preview.rb +82 -0
- data/lib/samagotchi/engine.rb +236 -443
- data/lib/samagotchi/gem_update.rb +89 -0
- data/lib/samagotchi/guardrails/approval.rb +26 -4
- data/lib/samagotchi/guardrails/load_failures.rb +9 -3
- data/lib/samagotchi/host_registry.rb +8 -12
- data/lib/samagotchi/idle_client.rb +24 -15
- data/lib/samagotchi/idle_reminders.rb +2 -2
- data/lib/samagotchi/image_store.rb +10 -6
- data/lib/samagotchi/kernel_loop.rb +59 -123
- data/lib/samagotchi/live_versions.rb +65 -0
- data/lib/samagotchi/llm/api_key.rb +41 -0
- data/lib/samagotchi/llm/chat_loop.rb +77 -13
- data/lib/samagotchi/llm/errors.rb +38 -7
- data/lib/samagotchi/llm/http.rb +19 -22
- data/lib/samagotchi/llm/openai_chat.rb +22 -26
- data/lib/samagotchi/memory_bundle/installer.rb +65 -63
- data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
- data/lib/samagotchi/memory_bundle/status.rb +4 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
- data/lib/samagotchi/model_profile.rb +27 -10
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/prompt.rb +4 -2
- data/lib/samagotchi/reminder_store.rb +1 -9
- data/lib/samagotchi/reply_wait.rb +48 -4
- data/lib/samagotchi/self_report.rb +37 -5
- data/lib/samagotchi/send_command.rb +190 -17
- data/lib/samagotchi/session.rb +4 -2
- data/lib/samagotchi/session_commands.rb +38 -8
- data/lib/samagotchi/session_manager.rb +19 -53
- data/lib/samagotchi/system_prompt.rb +403 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +141 -108
- data/lib/samagotchi/terminal_ui/attached_view.rb +27 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +29 -8
- data/lib/samagotchi/terminal_ui/formatting.rb +41 -22
- data/lib/samagotchi/terminal_ui/input_support.rb +7 -23
- data/lib/samagotchi/terminal_ui/plain_surface.rb +13 -7
- data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
- data/lib/samagotchi/terminal_ui/status_row.rb +81 -0
- data/lib/samagotchi/terminal_ui/surface.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +142 -923
- data/lib/samagotchi/text_diff.rb +181 -0
- data/lib/samagotchi/thinking.rb +126 -0
- data/lib/samagotchi/tool_activity.rb +52 -2
- data/lib/samagotchi/tool_runner.rb +37 -1
- data/lib/samagotchi/tools/ask_user_question.rb +41 -33
- data/lib/samagotchi/tools/edit.rb +23 -9
- data/lib/samagotchi/tools/execute.rb +3 -3
- data/lib/samagotchi/tools/output_guardrails.rb +8 -7
- data/lib/samagotchi/tools/read.rb +4 -4
- data/lib/samagotchi/tools/write.rb +4 -0
- data/lib/samagotchi/turn_flow.rb +12 -2
- data/lib/samagotchi/update_command.rb +309 -0
- data/lib/samagotchi/update_hint.rb +59 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +6 -4
- data/lib/samagotchi/web/app.rb +173 -38
- data/lib/samagotchi/web/lan.rb +99 -0
- data/lib/samagotchi/web/message_parts.rb +19 -10
- data/lib/samagotchi/web/public/activity.js +10 -0
- data/lib/samagotchi/web/public/app.js +135 -78
- data/lib/samagotchi/web/public/chat_view.js +8 -1
- data/lib/samagotchi/web/public/data.js +2 -0
- data/lib/samagotchi/web/public/diff_view.js +58 -0
- data/lib/samagotchi/web/public/index.html +185 -18
- data/lib/samagotchi/web/public/model_pick.js +136 -0
- data/lib/samagotchi/web/public/model_picker.js +224 -0
- data/lib/samagotchi/web/public/notify.js +10 -0
- data/lib/samagotchi/web/public/question_card.js +3 -1
- data/lib/samagotchi/web/public/stage_model.js +110 -0
- data/lib/samagotchi/web/public/stage_view.js +580 -0
- data/lib/samagotchi/web/public/timing.js +6 -2
- data/lib/samagotchi/web/public/turn_events.js +38 -10
- data/lib/samagotchi/web/public/turn_model.js +11 -3
- data/lib/samagotchi/web/public/turn_view.js +76 -20
- data/lib/samagotchi/web/qr.rb +40 -0
- data/lib/samagotchi/web/server.rb +101 -11
- data/lib/samagotchi/web/token.rb +97 -0
- data/lib/samagotchi/worker.rb +5 -4
- metadata +38 -3
- data/lib/samagotchi/terminal_ui/legacy_surface.rb +0 -111
|
@@ -13,13 +13,14 @@ module Samagotchi
|
|
|
13
13
|
|
|
14
14
|
module_function
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# The config setting +key+ (read.*, execute.*, context.*) when positive, else +default+.
|
|
17
|
+
def config_positive_int(key, default)
|
|
18
|
+
value = Samagotchi::Config.get(key).to_i
|
|
18
19
|
value.positive? ? value : default
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
def
|
|
22
|
-
value =
|
|
22
|
+
def config_positive_float(key, default)
|
|
23
|
+
value = Samagotchi::Config.get(key).to_f
|
|
23
24
|
value.positive? ? value : default
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -57,10 +58,10 @@ module Samagotchi
|
|
|
57
58
|
build_preview_parts(head: head, tail: tail, total_bytes: content.bytesize)
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
def telemetry_lines_for(content:,
|
|
61
|
-
chars_per_token =
|
|
61
|
+
def telemetry_lines_for(content:, threshold_key:, threshold_default:, token_key:, pct_key:)
|
|
62
|
+
chars_per_token = config_positive_float("context.chars_per_token", DEFAULT_CHARS_PER_TOKEN)
|
|
62
63
|
window_tokens = ContextWindow.current.tokens
|
|
63
|
-
telemetry_threshold_pct =
|
|
64
|
+
telemetry_threshold_pct = config_positive_float(threshold_key, threshold_default)
|
|
64
65
|
|
|
65
66
|
estimated_tokens = (content.length / chars_per_token).ceil
|
|
66
67
|
estimated_window_pct = (estimated_tokens.to_f / window_tokens) * 100.0
|
|
@@ -52,9 +52,9 @@ module Samagotchi
|
|
|
52
52
|
|
|
53
53
|
size = File.size(path)
|
|
54
54
|
|
|
55
|
-
hard_max_bytes = OutputGuardrails.
|
|
56
|
-
truncate_at_bytes = OutputGuardrails.
|
|
57
|
-
preview_bytes = OutputGuardrails.
|
|
55
|
+
hard_max_bytes = OutputGuardrails.config_positive_int("read.hard_max_bytes", DEFAULT_HARD_MAX_BYTES)
|
|
56
|
+
truncate_at_bytes = OutputGuardrails.config_positive_int("read.truncate_at_bytes", OutputGuardrails::DEFAULT_TRUNCATE_AT_BYTES)
|
|
57
|
+
preview_bytes = OutputGuardrails.config_positive_int("read.preview_bytes", OutputGuardrails::DEFAULT_PREVIEW_BYTES)
|
|
58
58
|
|
|
59
59
|
if size > hard_max_bytes
|
|
60
60
|
return "Error: file too large: #{path} (#{size} bytes, hard limit #{hard_max_bytes} bytes)."
|
|
@@ -105,7 +105,7 @@ module Samagotchi
|
|
|
105
105
|
def self.telemetry_lines_for(content)
|
|
106
106
|
OutputGuardrails.telemetry_lines_for(
|
|
107
107
|
content: content,
|
|
108
|
-
|
|
108
|
+
threshold_key: "read.telemetry_threshold_pct",
|
|
109
109
|
threshold_default: OutputGuardrails::DEFAULT_TELEMETRY_THRESHOLD_PCT,
|
|
110
110
|
token_key: "estimated_tokens_for_preview",
|
|
111
111
|
pct_key: "estimated_window_pct_for_preview"
|
|
@@ -14,6 +14,10 @@ module Samagotchi
|
|
|
14
14
|
def self.name = NAME
|
|
15
15
|
|
|
16
16
|
def self.call(content, path:)
|
|
17
|
+
# A native call without content/text arrives as nil; check before
|
|
18
|
+
# touching the file, or File.write(path, nil) would empty it.
|
|
19
|
+
return "Error: missing content" unless content.is_a?(String)
|
|
20
|
+
|
|
17
21
|
path = ToolPath.normalize(path)
|
|
18
22
|
FileUtils.mkdir_p(File.dirname(path))
|
|
19
23
|
File.write(path, content)
|
data/lib/samagotchi/turn_flow.rb
CHANGED
|
@@ -143,9 +143,19 @@ module Samagotchi
|
|
|
143
143
|
@checkpoint = nil
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
# A reminder turn
|
|
146
|
+
# A reminder turn is about to run: a pending continue offer goes, as for
|
|
147
|
+
# a new prompt (the partial turn stays), so a later "no" can't roll the
|
|
148
|
+
# reminder's exchange back with it.
|
|
149
|
+
# @return [Boolean] whether an offer was pending
|
|
150
|
+
def before_reminder_turn
|
|
151
|
+
offered = awaiting_continue?
|
|
152
|
+
@offer = nil
|
|
153
|
+
offered
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A reminder turn ran: the rollback window closes.
|
|
147
157
|
def after_reminder_turn
|
|
148
|
-
@checkpoint = nil
|
|
158
|
+
@checkpoint = nil
|
|
149
159
|
end
|
|
150
160
|
|
|
151
161
|
private
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config"
|
|
4
|
+
require_relative "desktop"
|
|
5
|
+
require_relative "gem_update"
|
|
6
|
+
require_relative "installed_gem"
|
|
7
|
+
require_relative "live_versions"
|
|
8
|
+
require_relative "memory_bundle/shipped_update"
|
|
9
|
+
require_relative "memory_bundle/system_bundle"
|
|
10
|
+
require_relative "session"
|
|
11
|
+
require_relative "version"
|
|
12
|
+
require_relative "web/lan"
|
|
13
|
+
|
|
14
|
+
module Samagotchi
|
|
15
|
+
# `chi update`: brings an installed chi up to date in one go: the system
|
|
16
|
+
# bundle, the shipped bundles the user installed, and the macOS helper.
|
|
17
|
+
# Unedited things update; edited ones are kept (.md) or replaced and
|
|
18
|
+
# reported (hooks, plugin, rules, which didn't load once edited). Live
|
|
19
|
+
# workers and a running chi web are reported, never restarted. Prints one
|
|
20
|
+
# table; a second run changes nothing and says so.
|
|
21
|
+
class UpdateCommand
|
|
22
|
+
USAGE = <<~TEXT
|
|
23
|
+
Usage: chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]
|
|
24
|
+
Updates this chi's install: the gem (from rubygems.org), then the
|
|
25
|
+
system bundle, the bundles chi ships that you installed (chi bundle
|
|
26
|
+
list), and the desktop helper (macOS; rebuilt only when its sources
|
|
27
|
+
changed). Edited memory files are kept and reported; nothing is
|
|
28
|
+
installed that isn't already, and old gem versions stay.
|
|
29
|
+
--dry-run show what would change; change nothing
|
|
30
|
+
--no-gem don't install a newer gem (config update.gem: false)
|
|
31
|
+
--no-bundles leave the shipped bundles alone (config update.bundles: false)
|
|
32
|
+
--no-desktop leave the desktop helper alone (config update.desktop: false)
|
|
33
|
+
Running sessions keep their chi until they idle out (30 min) or
|
|
34
|
+
chi sessions stop ID; a running chi web needs a restart.
|
|
35
|
+
TEXT
|
|
36
|
+
|
|
37
|
+
FLAGS = { "--dry-run" => :dry_run, "--no-gem" => :no_gem, "--no-bundles" => :no_bundles, "--no-desktop" => :no_desktop,
|
|
38
|
+
# Hidden: no pbs/lsregister/open/pkill for a helper under HOME=<tmp>.
|
|
39
|
+
"--no-register" => :no_register }.freeze
|
|
40
|
+
|
|
41
|
+
# A table row. info rows (live workers, chi web) say what runs; they
|
|
42
|
+
# never fail.
|
|
43
|
+
Row = Struct.new(:component, :from, :to, :status, :failed, keyword_init: true)
|
|
44
|
+
|
|
45
|
+
# What a row says after a change, and in a dry run.
|
|
46
|
+
VERBS = { update: ["updated", "would update"], install: ["installed", "would install"],
|
|
47
|
+
restore: ["restored missing files", "would restore missing files"] }.freeze
|
|
48
|
+
|
|
49
|
+
# @param gem_spec [Gem::Specification, nil] the installed gem (nil: a checkout)
|
|
50
|
+
# @param platform [#call] register: → a Desktop::MacOS-like object
|
|
51
|
+
# @param exec [#call] argv → replaces this process (the new chi's update)
|
|
52
|
+
# @param bundler [Boolean] running under Bundler (bundle exec)
|
|
53
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr, gem_spec: InstalledGem.spec,
|
|
54
|
+
shipped_dir: MemoryBundle::SourceNormalizer::SHIPPED_DIR, platform: nil,
|
|
55
|
+
supported: Desktop.supported?, state_dir: Session.default_state_dir, web: nil,
|
|
56
|
+
gem_update: GemUpdate.new, exec: nil, bundler: nil)
|
|
57
|
+
@gem_update = gem_update
|
|
58
|
+
@exec = exec || ->(argv) { Kernel.exec(*argv) }
|
|
59
|
+
@bundler = bundler.nil? ? (defined?(::Bundler) || !ENV["BUNDLE_GEMFILE"].to_s.empty?) : bundler
|
|
60
|
+
@argv = argv.dup
|
|
61
|
+
@stdout = stdout
|
|
62
|
+
@stderr = stderr
|
|
63
|
+
@gem_spec = gem_spec
|
|
64
|
+
@shipped_dir = shipped_dir
|
|
65
|
+
@platform = platform || ->(register:) { Desktop::MacOS.new(register: register) }
|
|
66
|
+
@supported = supported
|
|
67
|
+
@state_dir = state_dir
|
|
68
|
+
@web = web || [Web::Lan.local_host(Config.get("web.host")), Config.get("web.port").to_i]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Integer] 0 nothing failed, 1 a part failed (or a checkout), 2 usage
|
|
72
|
+
def run
|
|
73
|
+
options = parse or return 2
|
|
74
|
+
if options[:help]
|
|
75
|
+
@stdout.puts(USAGE)
|
|
76
|
+
return 0
|
|
77
|
+
end
|
|
78
|
+
unless @gem_spec
|
|
79
|
+
@stderr.puts("chi update: running from a checkout (#{InstalledGem::ROOT}): chi update updates an installed chi; " \
|
|
80
|
+
"git pull, or chi bundle upgrade NAME for one bundle")
|
|
81
|
+
return 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@dry_run = options[:dry_run]
|
|
85
|
+
gem = gem_row(options)
|
|
86
|
+
rows = [gem, system_bundle_row, *bundle_rows(options), desktop_row(options), *live_rows].compact
|
|
87
|
+
print_table(rows)
|
|
88
|
+
footers(options).each { |line| @stdout.puts(line) }
|
|
89
|
+
@stdout.puts(summary(rows))
|
|
90
|
+
rows.any?(&:failed) ? 1 : 0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def parse
|
|
96
|
+
argv = @argv.dup
|
|
97
|
+
options = {}
|
|
98
|
+
while (arg = argv.shift)
|
|
99
|
+
if %w[-h --help help].include?(arg)
|
|
100
|
+
options[:help] = true
|
|
101
|
+
elsif arg == "--gem-from" && argv.first
|
|
102
|
+
# Hidden: the old version, passed by the chi that installed this one.
|
|
103
|
+
options[:gem_from] = argv.shift
|
|
104
|
+
elsif FLAGS.key?(arg)
|
|
105
|
+
options[FLAGS[arg]] = true
|
|
106
|
+
else
|
|
107
|
+
@stderr.puts("chi update: unknown option #{arg}")
|
|
108
|
+
@stderr.puts(USAGE)
|
|
109
|
+
return nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
options
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Installs a newer gem and hands over to it (exec: the new code syncs
|
|
116
|
+
# and prints the table), or says why not. A failed install is a failed
|
|
117
|
+
# row; the sync then runs on this version.
|
|
118
|
+
def gem_row(options)
|
|
119
|
+
row = Row.new(component: "chi (gem)", from: VERSION)
|
|
120
|
+
if options[:gem_from]
|
|
121
|
+
row.from = options[:gem_from]
|
|
122
|
+
row.to = VERSION
|
|
123
|
+
return done(row, :update)
|
|
124
|
+
end
|
|
125
|
+
if (why = off(options, "gem"))
|
|
126
|
+
return skipped(row, why)
|
|
127
|
+
end
|
|
128
|
+
return skipped(row, "under Bundler: bundle update samagotchi") if @bundler
|
|
129
|
+
|
|
130
|
+
latest = begin
|
|
131
|
+
@gem_update.latest
|
|
132
|
+
rescue GemUpdate::Error => e
|
|
133
|
+
return row.tap { |r| r.status = "couldn't check (#{e.message})" }
|
|
134
|
+
end
|
|
135
|
+
return up_to_date(row) unless @gem_update.newer?(latest)
|
|
136
|
+
|
|
137
|
+
row.to = latest.version
|
|
138
|
+
return done(row, :update) if @dry_run
|
|
139
|
+
|
|
140
|
+
@stdout.puts("installing samagotchi #{latest.version}…")
|
|
141
|
+
output, ok = @gem_update.install(latest)
|
|
142
|
+
return failed(row, output.lines.map(&:strip).reject(&:empty?).last.to_s) unless ok
|
|
143
|
+
|
|
144
|
+
hand_over(options, row)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# exec the wrapper, which activates the newest installed samagotchi.
|
|
148
|
+
# Without one (installed with --bindir), this version syncs.
|
|
149
|
+
def hand_over(options, row)
|
|
150
|
+
wrapper = InstalledGem.wrapper(@gem_spec)
|
|
151
|
+
return done(row, :update, "run chi update again to sync with it") unless wrapper
|
|
152
|
+
|
|
153
|
+
passed = FLAGS.select { |_, key| options[key] && key != :dry_run }.keys
|
|
154
|
+
@stdout.flush
|
|
155
|
+
@exec.call([RbConfig.ruby, wrapper, "update", "--no-gem", "--gem-from", VERSION, *passed])
|
|
156
|
+
done(row, :update) # only when exec is a spec's stand-in
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Why a part is off for this run, or nil.
|
|
160
|
+
def off(options, part)
|
|
161
|
+
return "--no-#{part}" if options[:"no_#{part}"]
|
|
162
|
+
return "update.#{part}: false" unless Config.get("update.#{part}")
|
|
163
|
+
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def system_bundle_row
|
|
168
|
+
result = MemoryBundle::SystemBundle.sync(dry_run: @dry_run)
|
|
169
|
+
row = Row.new(component: "system bundle", from: result.from, to: result.to)
|
|
170
|
+
case result.status
|
|
171
|
+
when :installed then done(row, :install)
|
|
172
|
+
when :updated then done(row, :update, kept(result.kept, MemoryBundle::SystemBundle::BUNDLE_NAME))
|
|
173
|
+
when :restored then done(row, :restore).tap { |r| r.to = nil }
|
|
174
|
+
when :up_to_date then up_to_date(row)
|
|
175
|
+
when :newer_installed then skipped(row, "newer than this chi's #{result.to}: left").tap { |r| r.to = nil }
|
|
176
|
+
when :skipped then skipped(row, result.error)
|
|
177
|
+
else failed(row, result.error)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def bundle_rows(options)
|
|
182
|
+
if (why = off(options, "bundles"))
|
|
183
|
+
return [Row.new(component: "bundles", status: "skipped (#{why})")]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
rows = MemoryBundle::ShippedUpdate.plan(shipped_dir: @shipped_dir)
|
|
187
|
+
rows = MemoryBundle::ShippedUpdate.apply(rows) unless @dry_run
|
|
188
|
+
rows.map { |r| bundle_row(r) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def bundle_row(update)
|
|
192
|
+
row = Row.new(component: update.name, from: update.from, to: update.to)
|
|
193
|
+
case update.status
|
|
194
|
+
when :would_update, :updated
|
|
195
|
+
notes = [kept(update.kept, update.name), replaced(update.replaced)].compact
|
|
196
|
+
done(row, :update, notes.empty? ? nil : notes.join("; "))
|
|
197
|
+
when :up_to_date then up_to_date(row)
|
|
198
|
+
when :skipped then skipped(row, update.note)
|
|
199
|
+
else failed(row, update.note)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def desktop_row(options)
|
|
204
|
+
return nil unless @supported
|
|
205
|
+
|
|
206
|
+
row = Row.new(component: "Chi Helper")
|
|
207
|
+
if (why = off(options, "desktop"))
|
|
208
|
+
return skipped(row, why)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
helper = @platform.call(register: !options[:no_register])
|
|
212
|
+
return row.tap { |r| r.status = "not installed" } unless helper.installed?
|
|
213
|
+
|
|
214
|
+
row.from = helper.app_version
|
|
215
|
+
if helper.stale?
|
|
216
|
+
row.to = VERSION
|
|
217
|
+
return done(row, :update, @dry_run ? "rebuilds and restarts it" : "restarted") { helper.upgrade { nil } }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
return up_to_date(row) unless helper.launch_outdated?
|
|
221
|
+
|
|
222
|
+
helper.refresh_launch_file unless @dry_run
|
|
223
|
+
up_to_date(row).tap { |r| r.status += @dry_run ? " (would refresh the launch file)" : " (launch file refreshed)" }
|
|
224
|
+
rescue Desktop::MacOS::Error, SystemCallError => e
|
|
225
|
+
failed(row, e.message.lines.first.to_s.strip)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def live_rows
|
|
229
|
+
rows = []
|
|
230
|
+
stale = LiveVersions.stale_workers(VERSION, state_dir: @state_dir)
|
|
231
|
+
unless stale.empty?
|
|
232
|
+
versions = stale.map { |w| w.version || "an older chi" }.uniq.join(", ")
|
|
233
|
+
ids = stale.map { |w| w.session_id[0, 8] }.join(" ")
|
|
234
|
+
rows << Row.new(component: "workers",
|
|
235
|
+
status: "#{stale.size} live on #{versions}: they move to #{VERSION} at idle exit (30 min) " \
|
|
236
|
+
"or chi sessions stop #{ids}")
|
|
237
|
+
end
|
|
238
|
+
host, port = @web
|
|
239
|
+
web = LiveVersions.web_version(host, port)
|
|
240
|
+
if web && web != VERSION
|
|
241
|
+
rows << Row.new(component: "chi web :#{port}", from: web,
|
|
242
|
+
status: "running old code: new sessions it starts run #{web} too; restart it")
|
|
243
|
+
end
|
|
244
|
+
rows
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def footers(options)
|
|
248
|
+
return [] if off(options, "bundles")
|
|
249
|
+
|
|
250
|
+
names = MemoryBundle::ShippedUpdate.not_installed(shipped_dir: @shipped_dir).map(&:source)
|
|
251
|
+
names.empty? ? [] : ["Also shipped, not installed: #{names.join(", ")} (chi bundle install NAME)"]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def summary(rows)
|
|
255
|
+
return "(dry run: nothing was changed)" if @dry_run
|
|
256
|
+
return "some parts failed (see above)" if rows.any?(&:failed)
|
|
257
|
+
|
|
258
|
+
rows.all? { |r| r.status.start_with?("up to date", "skipped", "not installed", "couldn't check") } ? "everything is up to date" : "done"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# "updated" (or "would update" in a dry run), with a note; the block
|
|
262
|
+
# makes the change (not in a dry run).
|
|
263
|
+
def done(row, verb, note = nil)
|
|
264
|
+
yield if block_given? && !@dry_run
|
|
265
|
+
text = VERBS.fetch(verb)[@dry_run ? 1 : 0]
|
|
266
|
+
row.status = note ? "#{text} (#{note})" : text
|
|
267
|
+
row
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def up_to_date(row)
|
|
271
|
+
row.to = nil
|
|
272
|
+
row.status = "up to date"
|
|
273
|
+
row
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def skipped(row, why)
|
|
277
|
+
row.status = "skipped (#{why})"
|
|
278
|
+
row
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def failed(row, why)
|
|
282
|
+
row.status = "failed (#{why})"
|
|
283
|
+
row.failed = true
|
|
284
|
+
row
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def kept(files, bundle)
|
|
288
|
+
return nil if files.empty?
|
|
289
|
+
|
|
290
|
+
verb = @dry_run ? "would keep" : "kept"
|
|
291
|
+
"#{verb} your edits in #{files.join(", ")}: chi bundle diff #{bundle} #{files.first}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def replaced(files)
|
|
295
|
+
return nil if files.empty?
|
|
296
|
+
|
|
297
|
+
"#{@dry_run ? "would replace" : "replaced"} edited #{files.join(", ")} (it wasn't loading)"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def print_table(rows)
|
|
301
|
+
header = %w[component from to status]
|
|
302
|
+
cells = rows.map { |r| [r.component, r.from.to_s, r.to.to_s, r.status] }
|
|
303
|
+
widths = (0..2).map { |i| ([header] + cells).map { |c| c[i].length }.max }
|
|
304
|
+
([header] + cells).each do |c|
|
|
305
|
+
@stdout.puts((0..2).map { |i| c[i].ljust(widths[i]) }.join(" ") + " " + c[3])
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "desktop"
|
|
5
|
+
require_relative "memory_bundle/shipped_update"
|
|
6
|
+
require_relative "session"
|
|
7
|
+
require_relative "version"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
# One line at the first interactive start of a new chi version (REPL,
|
|
11
|
+
# attached, chi web) when something chi update would update is behind:
|
|
12
|
+
# "chi 0.3.1: 2 bundles and the desktop helper can be updated: chi update".
|
|
13
|
+
# Noted once per version in $XDG_STATE_HOME/samagotchi/update_noted,
|
|
14
|
+
# whether or not anything was behind. No network; nothing is changed (the
|
|
15
|
+
# system bundle syncs itself at every start).
|
|
16
|
+
module UpdateHint
|
|
17
|
+
NOTED_FILE = "update_noted"
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Only a person starting chi at a terminal: not -p or --non-interactive
|
|
22
|
+
# (a script), not a checkout (git pull updates it), never a worker
|
|
23
|
+
# (workers don't start through bin/chi's launch).
|
|
24
|
+
def wanted?(prompt:, non_interactive:, tty:, installed:)
|
|
25
|
+
!!(tty && installed && !prompt && !non_interactive)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param helper [#stale?, nil] the desktop helper (nil off macOS)
|
|
29
|
+
# @return [String, nil] the line shown, if any
|
|
30
|
+
def show(io: $stderr, env: ENV, version: VERSION, helper: default_helper(env),
|
|
31
|
+
shipped_dir: MemoryBundle::SourceNormalizer::SHIPPED_DIR)
|
|
32
|
+
path = File.join(File.dirname(Session.default_state_dir(env: env)), NOTED_FILE)
|
|
33
|
+
noted = File.exist?(path) ? File.readlines(path, chomp: true) : []
|
|
34
|
+
return nil if noted.include?(version)
|
|
35
|
+
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
37
|
+
File.write(path, "#{version}\n", mode: "a")
|
|
38
|
+
line = message(version, shipped_dir: shipped_dir, helper: helper)
|
|
39
|
+
io.puts(line) if line
|
|
40
|
+
line
|
|
41
|
+
rescue StandardError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def message(version, shipped_dir:, helper:)
|
|
46
|
+
bundles = MemoryBundle::ShippedUpdate.plan(shipped_dir: shipped_dir).count { |r| r.status == :would_update }
|
|
47
|
+
parts = []
|
|
48
|
+
parts << (bundles == 1 ? "1 bundle" : "#{bundles} bundles") if bundles.positive?
|
|
49
|
+
parts << "the desktop helper" if helper&.stale?
|
|
50
|
+
return nil if parts.empty?
|
|
51
|
+
|
|
52
|
+
"chi #{version}: #{parts.join(" and ")} can be updated: chi update"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def default_helper(env)
|
|
56
|
+
Desktop.supported? ? Desktop::MacOS.new(env: env) : nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -25,10 +25,12 @@ module Samagotchi
|
|
|
25
25
|
# @param profile [ModelProfile, nil] the model's prompt profile (native)
|
|
26
26
|
# @param adapter [LLM::OpenAIChat, nil] the chat host's adapter
|
|
27
27
|
# @param models [Hash, nil] ConfigFile.model_settings (specs)
|
|
28
|
+
# @param names [Array<String>, nil] the model's lookup names, the typed
|
|
29
|
+
# alias first (Engine#model_lookup_names); default: the target's names
|
|
28
30
|
# @return [Answer]
|
|
29
|
-
def self.for(target, profile: nil, adapter: nil, models: nil)
|
|
31
|
+
def self.for(target, profile: nil, adapter: nil, models: nil, names: nil)
|
|
30
32
|
entry = target.entry
|
|
31
|
-
configured = configured(target, entry, models)
|
|
33
|
+
configured = configured(target, entry, models, names)
|
|
32
34
|
return configured if configured&.no?
|
|
33
35
|
|
|
34
36
|
if entry.chat?
|
|
@@ -44,13 +46,13 @@ module Samagotchi
|
|
|
44
46
|
marker.is_a?(String) && !marker.empty? ? marker : nil
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
def self.configured(target, entry, models)
|
|
49
|
+
def self.configured(target, entry, models, names = nil)
|
|
48
50
|
models ||= begin
|
|
49
51
|
ConfigFile.model_settings
|
|
50
52
|
rescue StandardError
|
|
51
53
|
{}
|
|
52
54
|
end
|
|
53
|
-
key, value = ConfigFile.model_setting([target.model, target.bare_model], :vision, models: models)
|
|
55
|
+
key, value = ConfigFile.model_setting(names || [target.model, target.bare_model], :vision, models: models)
|
|
54
56
|
return Answer.new(value: value, reason: value ? nil : "models: #{key} sets vision: false") if key
|
|
55
57
|
return nil if entry.vision.nil?
|
|
56
58
|
|