samagotchi 0.3.0 → 0.4.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 +83 -1
- data/README.md +16 -0
- data/bin/chi +32 -31
- data/docs/cli.md +77 -5
- data/docs/configuration.md +104 -2
- data/docs/desktop.md +39 -4
- data/docs/guardrails.md +11 -0
- data/docs/hooks.md +88 -6
- data/docs/releasing.md +6 -6
- data/docs/sessions.md +19 -17
- data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
- data/lib/samagotchi/bridge.rb +4 -1
- 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 +2 -2
- data/lib/samagotchi/bundles/system/delegated.md +6 -7
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/client.rb +9 -6
- data/lib/samagotchi/commands/registry.rb +8 -0
- data/lib/samagotchi/config.rb +64 -20
- 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 +201 -104
- 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 +26 -79
- data/lib/samagotchi/live_versions.rb +59 -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 +21 -7
- data/lib/samagotchi/llm/http.rb +15 -4
- data/lib/samagotchi/llm/openai_chat.rb +5 -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 +23 -0
- data/lib/samagotchi/prompt.rb +4 -2
- data/lib/samagotchi/reminder_store.rb +1 -9
- data/lib/samagotchi/self_report.rb +17 -3
- data/lib/samagotchi/send_command.rb +107 -12
- data/lib/samagotchi/session_commands.rb +38 -8
- data/lib/samagotchi/session_manager.rb +1 -16
- data/lib/samagotchi/terminal_ui/attached_loop.rb +21 -25
- data/lib/samagotchi/terminal_ui/event_renderer.rb +8 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +9 -0
- data/lib/samagotchi/terminal_ui/input_support.rb +4 -19
- data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
- data/lib/samagotchi/terminal_ui.rb +62 -248
- data/lib/samagotchi/text_diff.rb +181 -0
- data/lib/samagotchi/thinking.rb +115 -0
- data/lib/samagotchi/tool_runner.rb +34 -1
- data/lib/samagotchi/tools/ask_user_question.rb +41 -33
- data/lib/samagotchi/tools/edit.rb +23 -9
- data/lib/samagotchi/tools/write.rb +4 -0
- data/lib/samagotchi/turn_flow.rb +12 -2
- data/lib/samagotchi/update_command.rb +308 -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 +3 -3
- data/lib/samagotchi/web/message_parts.rb +8 -3
- data/lib/samagotchi/web/public/activity.js +3 -0
- data/lib/samagotchi/web/public/app.js +36 -24
- data/lib/samagotchi/web/public/chat_view.js +3 -0
- 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 +22 -1
- data/lib/samagotchi/web/public/question_card.js +3 -1
- data/lib/samagotchi/web/public/turn_events.js +29 -5
- data/lib/samagotchi/web/public/turn_view.js +2 -1
- data/lib/samagotchi/worker.rb +5 -4
- metadata +12 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "installer"
|
|
6
|
+
require_relative "listing"
|
|
7
|
+
require_relative "manifest"
|
|
8
|
+
require_relative "merger"
|
|
9
|
+
require_relative "provenance"
|
|
10
|
+
require_relative "source"
|
|
11
|
+
require_relative "status"
|
|
12
|
+
require_relative "system_bundle"
|
|
13
|
+
require_relative "../version"
|
|
14
|
+
|
|
15
|
+
module Samagotchi
|
|
16
|
+
module MemoryBundle
|
|
17
|
+
# What `chi update` does to the installed bundles that chi ships: one
|
|
18
|
+
# Row per installed bundle (the system bundle aside: SystemBundle.sync
|
|
19
|
+
# has it). A bundle is updated when it was installed from a shipped dir
|
|
20
|
+
# (its source is …/lib/samagotchi/bundles/<dir>, in a gem or a
|
|
21
|
+
# checkout), chi ships it under the same name, the shipped version is
|
|
22
|
+
# newer and this chi meets its requires_chi. It never installs a
|
|
23
|
+
# bundle that isn't installed and never downgrades.
|
|
24
|
+
#
|
|
25
|
+
# .md files take the Installer's 3-way merge: a conflict keeps the
|
|
26
|
+
# user's file (kept). Hooks, rules and the plugin are replaced; an
|
|
27
|
+
# edited one (which didn't load: its sha no longer matched) is listed
|
|
28
|
+
# in replaced.
|
|
29
|
+
module ShippedUpdate
|
|
30
|
+
# status: :would_update (plan), :updated, :up_to_date, :skipped,
|
|
31
|
+
# :failed. note says why for skipped/failed. kept and replaced are
|
|
32
|
+
# file names; under plan they are what apply would keep or replace.
|
|
33
|
+
Row = Struct.new(:name, :from, :to, :status, :note, :kept, :replaced, :source_dir, :scope, keyword_init: true)
|
|
34
|
+
|
|
35
|
+
SHIPPED_SOURCE = %r{/lib/samagotchi/bundles/[^/]+/?\z}
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# Read-only.
|
|
40
|
+
# @return [Array<Row>] by name
|
|
41
|
+
def plan(shipped_dir: SourceNormalizer::SHIPPED_DIR, chi_version: Samagotchi::VERSION)
|
|
42
|
+
shipped = Listing.shipped(dir: shipped_dir).to_h { |s| [s.name, s] }
|
|
43
|
+
installed_names.map do |name|
|
|
44
|
+
plan_row(name, shipped[name], shipped_dir, chi_version)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The shipped bundles not installed (what the footer lists).
|
|
49
|
+
# @return [Array<Listing::Shipped>]
|
|
50
|
+
def not_installed(shipped_dir: SourceNormalizer::SHIPPED_DIR)
|
|
51
|
+
names = installed_names
|
|
52
|
+
Listing.shipped(dir: shipped_dir).reject { |s| names.include?(s.name) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Runs the Installer for each :would_update row; the others pass
|
|
56
|
+
# through. A failing bundle doesn't stop the rest.
|
|
57
|
+
# @return [Array<Row>]
|
|
58
|
+
def apply(rows)
|
|
59
|
+
rows.map do |row|
|
|
60
|
+
next row unless row.status == :would_update
|
|
61
|
+
|
|
62
|
+
installer = Installer.new(source: row.source_dir, name: row.name, scope: row.scope, strict: true, upgrade: true)
|
|
63
|
+
installer.run
|
|
64
|
+
row.dup.tap do |r|
|
|
65
|
+
r.status = :updated
|
|
66
|
+
r.kept = installer.conflicts.keys
|
|
67
|
+
end
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
row.dup.tap do |r|
|
|
70
|
+
r.status = :failed
|
|
71
|
+
r.note = e.message.lines.first.to_s.strip
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def installed_names
|
|
77
|
+
dir = Provenance.bundles_dir
|
|
78
|
+
return [] unless File.directory?(dir)
|
|
79
|
+
|
|
80
|
+
Dir.children(dir).reject { |e| e.start_with?(".") || e == SystemBundle::BUNDLE_NAME }
|
|
81
|
+
.select { |e| File.file?(File.join(dir, e, "manifest.json")) }.sort
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def plan_row(name, ship, shipped_dir, chi_version)
|
|
85
|
+
row = Row.new(name: name, kept: [], replaced: [])
|
|
86
|
+
data = begin
|
|
87
|
+
Provenance.new(name: name).read
|
|
88
|
+
rescue JSON::ParserError, SystemCallError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
return skip(row, "manifest.json unreadable") unless data.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
row.from = data[:version].to_s
|
|
94
|
+
row.scope = data[:scope].to_s.empty? ? "system" : data[:scope].to_s
|
|
95
|
+
return skip(row, "not from chi") unless ship && data[:source].to_s.match?(SHIPPED_SOURCE)
|
|
96
|
+
|
|
97
|
+
row.to = ship.version.to_s
|
|
98
|
+
return skip(row, "newer than shipped: left") if Listing.newer?(row.from, row.to)
|
|
99
|
+
return row.tap { |r| r.status = :up_to_date; r.to = nil } unless Listing.newer?(row.to, row.from)
|
|
100
|
+
|
|
101
|
+
row.source_dir = File.join(shipped_dir, ship.source)
|
|
102
|
+
manifest = Manifest.read(dir: row.source_dir)
|
|
103
|
+
if Manifest.requires_chi_failure(manifest.requires_chi, chi_version)
|
|
104
|
+
return skip(row, "needs chi #{manifest.requires_chi}", keep_to: true)
|
|
105
|
+
end
|
|
106
|
+
return skip(row, "project scope: chi bundle upgrade #{ship.source} in the project") if row.scope == "project"
|
|
107
|
+
|
|
108
|
+
row.kept = conflicts(row, data)
|
|
109
|
+
row.replaced = edited_executables(name, data)
|
|
110
|
+
row.status = :would_update
|
|
111
|
+
row
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def skip(row, note, keep_to: false)
|
|
115
|
+
row.to = nil unless keep_to
|
|
116
|
+
row.status = :skipped
|
|
117
|
+
row.note = note
|
|
118
|
+
row
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The .md files whose local edits conflict with the shipped version.
|
|
122
|
+
def conflicts(row, data)
|
|
123
|
+
provenance = Provenance.new(name: row.name)
|
|
124
|
+
target_dir = Status.resolve_target_dir(row.scope)
|
|
125
|
+
return [] unless data[:files]
|
|
126
|
+
|
|
127
|
+
Dir.glob(File.join(row.source_dir, "*.md")).map { |f| File.basename(f) }.sort.select do |key|
|
|
128
|
+
Merger.classify(base_path: provenance.base_path(key), current_path: File.join(target_dir, key),
|
|
129
|
+
incoming_path: File.join(row.source_dir, key)) == :conflict
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Installed hooks, rules and the plugin whose sha differs from the
|
|
134
|
+
# recorded one (they didn't load), as hooks/F, guardrails/F, plugin/F.
|
|
135
|
+
def edited_executables(name, data)
|
|
136
|
+
provenance = Provenance.new(name: name)
|
|
137
|
+
edited = []
|
|
138
|
+
(data[:hooks] || {}).each do |file, meta|
|
|
139
|
+
edited << "hooks/#{file}" if edited?(File.join(provenance.hooks_dir, file.to_s), meta)
|
|
140
|
+
end
|
|
141
|
+
(data[:guardrails] || {}).each do |file, meta|
|
|
142
|
+
edited << "guardrails/#{file}" if edited?(File.join(provenance.guardrails_dir, file.to_s), meta)
|
|
143
|
+
end
|
|
144
|
+
plugin = provenance.plugin_path(data)
|
|
145
|
+
edited << "plugin/#{File.basename(plugin)}" if plugin && edited?(plugin, data[:plugin])
|
|
146
|
+
edited
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def edited?(path, meta)
|
|
150
|
+
recorded = meta.is_a?(Hash) ? meta[:sha256].to_s.delete_prefix("sha256:") : ""
|
|
151
|
+
return false if recorded.empty? || !File.file?(path)
|
|
152
|
+
|
|
153
|
+
Digest::SHA256.hexdigest(File.binread(path)) != recorded
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -31,6 +31,7 @@ module Samagotchi
|
|
|
31
31
|
missing = !File.exist?(target_path)
|
|
32
32
|
index_present = index_has_entry?(scope, file_key_str)
|
|
33
33
|
details[file_key_str] = {
|
|
34
|
+
conflict: meta.is_a?(Hash) && meta[:conflict] == true,
|
|
34
35
|
stored_checksum: stored_checksum,
|
|
35
36
|
current_checksum: current_checksum,
|
|
36
37
|
base_checksum: base_checksum,
|
|
@@ -96,11 +97,13 @@ module Samagotchi
|
|
|
96
97
|
end
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
# Index lines name the entry without ".md" (Installer#update_target_index,
|
|
101
|
+
# memory_write); a legacy "name.md" line counts too.
|
|
99
102
|
def self.index_has_entry?(scope, file_key)
|
|
100
103
|
path = IndexUpdater.index_path_for(scope)
|
|
101
104
|
return false unless path && File.exist?(path)
|
|
102
105
|
content = File.read(path)
|
|
103
|
-
content.match?(IndexUpdater.managed_pattern(
|
|
106
|
+
[file_key.delete_suffix(".md"), file_key].uniq.any? { |name| content.match?(IndexUpdater.managed_pattern(name)) }
|
|
104
107
|
end
|
|
105
108
|
end
|
|
106
109
|
end
|
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require_relative "installer"
|
|
5
5
|
require_relative "provenance"
|
|
6
6
|
require_relative "manifest"
|
|
7
|
+
require_relative "merger"
|
|
7
8
|
require_relative "../log"
|
|
8
9
|
|
|
9
10
|
module Samagotchi
|
|
@@ -27,40 +28,72 @@ module Samagotchi
|
|
|
27
28
|
|
|
28
29
|
module_function
|
|
29
30
|
|
|
31
|
+
# What sync did (or, with dry_run, would do). status is :installed,
|
|
32
|
+
# :updated, :restored (missing files put back), :up_to_date,
|
|
33
|
+
# :newer_installed (left as is), :skipped (SKIP_ENV or no shipped
|
|
34
|
+
# bundle) or :failed (error: says why); kept lists the files whose
|
|
35
|
+
# local edits conflicted with the update and were kept.
|
|
36
|
+
Result = Struct.new(:from, :to, :status, :kept, :warnings, :error, keyword_init: true) do
|
|
37
|
+
def changed? = %i[installed updated restored].include?(status)
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
# Ensure the system bundle is installed and up-to-date.
|
|
31
41
|
# Idempotent and warn-only: never raises to callers.
|
|
32
42
|
# Safe for TUI and non-TUI (web/worker) paths — Engine calls this.
|
|
33
43
|
def ensure!
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
result = sync
|
|
45
|
+
case result.status
|
|
46
|
+
when :failed
|
|
47
|
+
Log.error(:memory, "system_bundle_failed", echo: "[samagotchi-system] #{result.error}")
|
|
48
|
+
when :newer_installed
|
|
49
|
+
with_lock { note_newer_installed(result.from, result.to) }
|
|
50
|
+
end
|
|
51
|
+
result.kept.each do |file_key|
|
|
52
|
+
Log.info(:memory, "system_local_edit_kept", echo: "[samagotchi-system] kept local edit in #{file_key} (bundle v#{result.to} has update — run: chi bundle status #{BUNDLE_NAME} / chi bundle diff #{BUNDLE_NAME} #{file_key})", file: file_key.to_s)
|
|
53
|
+
end
|
|
54
|
+
result.warnings.each { |w| Log.warn(:memory, "system_bundle_warning", echo: "[samagotchi-system] #{w}") }
|
|
55
|
+
result.changed?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Install, upgrade or repair the installed system bundle to match the
|
|
59
|
+
# shipped one, under the lock. Never raises; logs nothing (ensure!
|
|
60
|
+
# and chi update report the Result).
|
|
61
|
+
# @param dry_run [Boolean] classify only, write nothing
|
|
62
|
+
# @return [Result]
|
|
63
|
+
def sync(dry_run: false)
|
|
64
|
+
return result(:skipped, error: "#{SKIP_ENV} is set") if skip?
|
|
65
|
+
return result(:skipped, error: "no shipped system bundle") unless File.directory?(GEM_BUNDLE_DIR)
|
|
36
66
|
|
|
37
67
|
begin
|
|
38
68
|
gem_manifest = Manifest.read(dir: GEM_BUNDLE_DIR)
|
|
39
69
|
rescue Manifest::ValidationError => e
|
|
40
|
-
|
|
41
|
-
return false
|
|
70
|
+
return result(:failed, error: "invalid gem manifest: #{e.message}")
|
|
42
71
|
end
|
|
72
|
+
to = gem_manifest.version.to_s
|
|
43
73
|
|
|
44
74
|
with_lock do
|
|
45
75
|
data = Provenance.new(name: BUNDLE_NAME).read
|
|
76
|
+
from = data && data[:version].to_s
|
|
46
77
|
|
|
47
78
|
if data.nil?
|
|
48
|
-
install_fresh(
|
|
49
|
-
elsif installed_newer?(
|
|
79
|
+
dry_run ? result(:installed, to: to) : install_fresh(to)
|
|
80
|
+
elsif installed_newer?(from, to)
|
|
50
81
|
# Running an older checkout/gem: never downgrade the user's system memories
|
|
51
82
|
# (not even to restore a missing file). `chi self` shows installed vs shipped.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
upgrade_existing(gem_manifest, data)
|
|
83
|
+
result(:newer_installed, from: from, to: to)
|
|
84
|
+
elsif from != to
|
|
85
|
+
dry_run ? result(:updated, from: from, to: to, kept: conflicting(gem_manifest)) : upgrade_existing(from, to)
|
|
56
86
|
else
|
|
57
87
|
# Already at desired version — still verify files present (e.g. user deleted one)
|
|
58
|
-
verify_files_present(gem_manifest,
|
|
88
|
+
verify_files_present(gem_manifest, from, dry_run)
|
|
59
89
|
end
|
|
60
90
|
end
|
|
61
91
|
rescue StandardError => e
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
result(:failed, error: "ensure failed: #{e.class}: #{e.message}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def result(status, from: nil, to: nil, kept: [], warnings: [], error: nil)
|
|
96
|
+
Result.new(from: from, to: to, status: status, kept: kept, warnings: warnings, error: error)
|
|
64
97
|
end
|
|
65
98
|
|
|
66
99
|
# Parallel chi starts on one config dir take turns: the second one reads
|
|
@@ -101,7 +134,7 @@ module Samagotchi
|
|
|
101
134
|
File.write(path, "#{pair}\n", mode: "a")
|
|
102
135
|
end
|
|
103
136
|
|
|
104
|
-
def install_fresh(
|
|
137
|
+
def install_fresh(to)
|
|
105
138
|
installer = Installer.new(
|
|
106
139
|
source: GEM_BUNDLE_DIR,
|
|
107
140
|
name: BUNDLE_NAME,
|
|
@@ -109,53 +142,48 @@ module Samagotchi
|
|
|
109
142
|
force: false,
|
|
110
143
|
strict: true
|
|
111
144
|
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
installer.warnings.each { |w| Log.warn(:memory, "system_bundle_warning", echo: "[samagotchi-system] #{w}") }
|
|
115
|
-
end
|
|
116
|
-
true
|
|
145
|
+
installer.run
|
|
146
|
+
result(:installed, to: to, warnings: installer.warnings)
|
|
117
147
|
end
|
|
118
148
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
128
|
-
_nd, _manifest = installer.run
|
|
129
|
-
# Installer already handled fast_forward/keep/noop. Conflicts are kept with warning.
|
|
130
|
-
if installer.conflicts.any?
|
|
131
|
-
installer.conflicts.each do |file_key, _info|
|
|
132
|
-
Log.info(:memory, "system_local_edit_kept", echo: "[samagotchi-system] kept local edit in #{file_key} (bundle v#{gem_manifest.version} has update — run: chi bundle status #{BUNDLE_NAME} / chi bundle diff #{BUNDLE_NAME} #{file_key})", file: file_key.to_s)
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
installer.warnings.each { |w| Log.warn(:memory, "system_bundle_warning", echo: "[samagotchi-system] #{w}") } unless installer.warnings.empty?
|
|
136
|
-
true
|
|
149
|
+
# Installer handles fast_forward/keep/noop; a conflict keeps the local
|
|
150
|
+
# file, and its own "Conflict in" warning is left out (kept says it).
|
|
151
|
+
def upgrade_existing(from, to)
|
|
152
|
+
installer = upgrade_installer
|
|
153
|
+
installer.run
|
|
154
|
+
kept = installer.conflicts.keys
|
|
155
|
+
warnings = installer.warnings.reject { |w| kept.any? { |k| w.start_with?("Conflict in #{k}:") } }
|
|
156
|
+
result(:updated, from: from, to: to, kept: kept, warnings: warnings)
|
|
137
157
|
end
|
|
138
158
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
# A file from the bundle missing on disk (the user deleted it) is put
|
|
160
|
+
# back via the Installer (which classifies it as :install), without a
|
|
161
|
+
# full reinstall.
|
|
162
|
+
def verify_files_present(gem_manifest, version, dry_run)
|
|
143
163
|
target_dir = Installer.system_dir
|
|
144
164
|
missing = gem_manifest.files.keys.any? { |k| !File.exist?(File.join(target_dir, k)) }
|
|
145
|
-
return
|
|
165
|
+
return result(:up_to_date, from: version, to: version) unless missing
|
|
166
|
+
return result(:restored, from: version, to: version) if dry_run
|
|
146
167
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
168
|
+
upgrade_installer.run
|
|
169
|
+
result(:restored, from: version, to: version)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The files an upgrade would keep: edited here and changed in the
|
|
173
|
+
# shipped bundle (a dry run's kept).
|
|
174
|
+
def conflicting(gem_manifest)
|
|
175
|
+
provenance = Provenance.new(name: BUNDLE_NAME)
|
|
176
|
+
gem_manifest.files.keys.map(&:to_s).sort.select do |key|
|
|
177
|
+
Merger.classify(base_path: provenance.base_path(key), current_path: File.join(Installer.system_dir, key),
|
|
178
|
+
incoming_path: File.join(GEM_BUNDLE_DIR, key)) == :conflict
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def upgrade_installer
|
|
183
|
+
Installer.new(source: GEM_BUNDLE_DIR, name: BUNDLE_NAME, scope: SCOPE, force: false, strict: true, upgrade: true)
|
|
157
184
|
end
|
|
158
|
-
private_class_method :with_lock, :note_newer_installed, :install_fresh, :upgrade_existing, :verify_files_present
|
|
185
|
+
private_class_method :with_lock, :note_newer_installed, :install_fresh, :upgrade_existing, :verify_files_present,
|
|
186
|
+
:upgrade_installer, :result, :conflicting
|
|
159
187
|
end
|
|
160
188
|
end
|
|
161
189
|
end
|
|
@@ -14,6 +14,9 @@ module Samagotchi
|
|
|
14
14
|
# No model anywhere (--model, default.model in config.yml, the env): a
|
|
15
15
|
# first run before any config. An ArgumentError, as before.
|
|
16
16
|
MissingModel = Class.new(ArgumentError)
|
|
17
|
+
# A host-qualified model whose host isn't configured (a MissingModel,
|
|
18
|
+
# so every surface that reports a missing model reports it the same way).
|
|
19
|
+
UnknownHost = Class.new(MissingModel)
|
|
17
20
|
|
|
18
21
|
attr_reader :name, :turn_start, :turn_end,
|
|
19
22
|
:tool_call_open, :tool_call_close,
|
|
@@ -130,6 +133,26 @@ module Samagotchi
|
|
|
130
133
|
value
|
|
131
134
|
end
|
|
132
135
|
|
|
136
|
+
# Raises UnknownHost when +model_name+ (or the alias it names) is
|
|
137
|
+
# qualified with a host that isn't configured, instead of sending the
|
|
138
|
+
# whole ref to the default host as a model id. Where a model comes in
|
|
139
|
+
# (an Engine starting or switching, a spawned session) calls it.
|
|
140
|
+
# @param hosts [Hash, nil] the hosts a prefix may name (a HostRegistry's
|
|
141
|
+
# entries); config.yml's by default
|
|
142
|
+
# @return [String] +model_name+
|
|
143
|
+
def self.check_host!(model_name, env: ENV, hosts: nil)
|
|
144
|
+
require_relative "config"
|
|
145
|
+
hosts ||= Samagotchi::ConfigFile.hosts_config(env: env)
|
|
146
|
+
ref = Samagotchi::ConfigFile.resolve_model_alias(model_name, env: env, hosts: hosts)
|
|
147
|
+
host = Samagotchi::ConfigFile.unknown_host_prefix(ref, hosts: hosts)
|
|
148
|
+
return model_name unless host
|
|
149
|
+
|
|
150
|
+
names = hosts.keys.map { |k| k.to_s.downcase }.sort
|
|
151
|
+
near = Samagotchi::Config.near_names(host, names).first(3)
|
|
152
|
+
hint = near.empty? ? "" : " (did you mean #{near.map { |n| "'#{n}'" }.join(' or ')}?)"
|
|
153
|
+
raise UnknownHost, "unknown host '#{host}' in model '#{ref}'#{hint}; the configured hosts are #{names.join(', ')}"
|
|
154
|
+
end
|
|
155
|
+
|
|
133
156
|
# One line for the user: where to set the model.
|
|
134
157
|
def self.missing_model_message
|
|
135
158
|
path = begin
|
data/lib/samagotchi/prompt.rb
CHANGED
|
@@ -39,8 +39,10 @@ module Samagotchi
|
|
|
39
39
|
# what it was before images existed.
|
|
40
40
|
#
|
|
41
41
|
# @param vision [VisionContext, nil] the turn's images context
|
|
42
|
+
# @param prefill [String] text after the generation cue, the start of
|
|
43
|
+
# the model's answer (Thinking.native: Qwen's empty thought)
|
|
42
44
|
# @return [Array(String, Array<String>)]
|
|
43
|
-
def self.format_with_images(messages, profile: ModelProfile.default, vision: nil)
|
|
45
|
+
def self.format_with_images(messages, profile: ModelProfile.default, vision: nil, prefill: "")
|
|
44
46
|
plan = ImagePlan.new(messages, vision, data: :base64)
|
|
45
47
|
images = []
|
|
46
48
|
suffixes = messages.each_with_index.map { |m, index| image_lines(plan.items(m, index), profile, images) }
|
|
@@ -49,7 +51,7 @@ module Samagotchi
|
|
|
49
51
|
else
|
|
50
52
|
format_with_turn_markers(messages, profile, suffixes)
|
|
51
53
|
end
|
|
52
|
-
[text, images]
|
|
54
|
+
[text + prefill.to_s, images]
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
private
|
|
@@ -90,7 +90,7 @@ module Samagotchi
|
|
|
90
90
|
|
|
91
91
|
# Get all due reminders whose interval has elapsed.
|
|
92
92
|
# Returns an Array of {name:, description:, interval_minutes:} hashes.
|
|
93
|
-
# Called by Engine#
|
|
93
|
+
# Called by Engine#collect_due_reminders (synchronously, at run_turn start).
|
|
94
94
|
# Thread-safe. Returns a frozen copy so the caller can't mutate internal state.
|
|
95
95
|
# @return [Array<Hash>] array of due reminder hashes (may be empty)
|
|
96
96
|
def due_reminders
|
|
@@ -103,14 +103,6 @@ module Samagotchi
|
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
# Get the name of the next due reminder (if any) whose interval has elapsed.
|
|
107
|
-
# Kept for backward compatibility with IdleReminders thread.
|
|
108
|
-
# @return [String, nil] the reminder name, or nil if none are due
|
|
109
|
-
def next_due_name
|
|
110
|
-
due = due_reminders
|
|
111
|
-
due.first&.fetch(:name)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
106
|
# Mark a reminder as fired — resets its next_fire_at to now + interval.
|
|
115
107
|
# Called by Engine after a due reminder has been delivered.
|
|
116
108
|
# @param name [String] the reminder name
|
|
@@ -7,6 +7,7 @@ require_relative "context_window"
|
|
|
7
7
|
require_relative "session"
|
|
8
8
|
require_relative "log_path"
|
|
9
9
|
require_relative "model_profile"
|
|
10
|
+
require_relative "thinking"
|
|
10
11
|
require_relative "served_model"
|
|
11
12
|
require_relative "host_registry"
|
|
12
13
|
require_relative "tools/memory"
|
|
@@ -50,6 +51,7 @@ module Samagotchi
|
|
|
50
51
|
["api key", model ? api_key_for(model, env) : "-"],
|
|
51
52
|
["loop", model ? loop_for(model, env) : "-"],
|
|
52
53
|
["profile", model ? profile_for(model, env) : "-"],
|
|
54
|
+
["thinking", model ? thinking_for(model, env) : "-"],
|
|
53
55
|
["served model", model ? served_model_for(model, env) : "-"],
|
|
54
56
|
["context window", context_window(env)],
|
|
55
57
|
["bundles", bundles_summary],
|
|
@@ -150,6 +152,15 @@ module Samagotchi
|
|
|
150
152
|
end
|
|
151
153
|
end
|
|
152
154
|
|
|
155
|
+
# The model's thinking level and where it came from (Thinking.resolve).
|
|
156
|
+
def thinking_for(model, env)
|
|
157
|
+
target = HostRegistry.new(env: env).resolve(model)
|
|
158
|
+
level, source = Thinking.resolve(target, models: ConfigFile.model_settings(env: env))
|
|
159
|
+
source ? "#{level} (#{source})" : level.to_s
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
"(unknown: #{e.message})"
|
|
162
|
+
end
|
|
163
|
+
|
|
153
164
|
def host_for(model, env)
|
|
154
165
|
entry, bare = HostRegistry.new(env: env).host_for_model(model)
|
|
155
166
|
return "(no hosts configured)" unless entry
|
|
@@ -170,14 +181,17 @@ module Samagotchi
|
|
|
170
181
|
end
|
|
171
182
|
|
|
172
183
|
# The Chi Helper app's version against this chi's (read from its
|
|
173
|
-
# Info.plist; no process or Services checks: chi desktop
|
|
184
|
+
# Info.plist and launch file; no process or Services checks: chi desktop
|
|
185
|
+
# status has those). An older app is fine while its sources are unchanged.
|
|
174
186
|
def desktop_summary(env)
|
|
175
187
|
return "- (macOS only)" unless Desktop.supported?
|
|
176
188
|
|
|
177
|
-
|
|
189
|
+
helper = Desktop::MacOS.new(env: env)
|
|
190
|
+
version = helper.app_version
|
|
178
191
|
return "not installed" unless version
|
|
192
|
+
return "#{version} (matches)" if version == VERSION
|
|
179
193
|
|
|
180
|
-
|
|
194
|
+
helper.stale? ? "#{version} (chi is #{VERSION}: chi update)" : "#{version} (up to date for chi #{VERSION})"
|
|
181
195
|
end
|
|
182
196
|
|
|
183
197
|
# "samagotchi-system 0.1.5 (shipped 0.1.5), other 1.0.0"
|