samagotchi 0.151.0 → 0.153.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 +46 -1
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +84 -0
- data/docs/cli.md +35 -10
- data/docs/configuration.md +19 -14
- data/docs/guardrails.md +11 -5
- data/docs/hooks.md +4 -3
- data/docs/internals/anthropic.md +3 -3
- data/docs/internals/attached-context.md +3 -0
- data/docs/internals/block-renderers.md +3 -1
- data/docs/internals/bundles.md +48 -14
- data/docs/internals/engine.md +9 -5
- data/docs/internals/objectives.md +6 -4
- data/docs/internals/participants.md +1 -1
- data/docs/internals/prompt-caching.md +7 -3
- data/docs/internals/web-frontend.md +20 -17
- data/docs/memory.md +6 -1
- data/docs/plugins.md +80 -29
- data/docs/releasing.md +8 -3
- data/docs/sessions.md +2 -2
- data/docs/testing.md +2 -2
- data/lib/samagotchi/block_renderers.rb +9 -8
- data/lib/samagotchi/bundle_command/check.rb +43 -3
- data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
- data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
- data/lib/samagotchi/bundle_command.rb +88 -10
- data/lib/samagotchi/bundle_needs.rb +4 -6
- data/lib/samagotchi/bundle_settings_check.rb +14 -13
- data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
- data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
- data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
- data/lib/samagotchi/child_env.rb +15 -0
- data/lib/samagotchi/context_providers.rb +42 -10
- data/lib/samagotchi/engine.rb +37 -9
- data/lib/samagotchi/extension_load.rb +4 -3
- data/lib/samagotchi/guardrail_wiring.rb +28 -19
- data/lib/samagotchi/guardrails/approval.rb +2 -2
- data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
- data/lib/samagotchi/guardrails/rules.rb +1 -0
- data/lib/samagotchi/guardrails/verdict.rb +20 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
- data/lib/samagotchi/llm_context_stale.rb +2 -2
- data/lib/samagotchi/memory_bundle/asset_installer.rb +11 -8
- data/lib/samagotchi/memory_bundle/builder.rb +36 -30
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
- data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
- data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -46
- data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -15
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
- data/lib/samagotchi/memory_bundle/installer.rb +90 -16
- data/lib/samagotchi/memory_bundle/manifest.rb +146 -6
- data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
- data/lib/samagotchi/memory_bundle/merger.rb +7 -8
- data/lib/samagotchi/memory_bundle/profile.rb +3 -2
- data/lib/samagotchi/memory_bundle/provenance.rb +217 -18
- data/lib/samagotchi/memory_bundle/shas.rb +4 -3
- data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
- data/lib/samagotchi/memory_bundle/status.rb +11 -10
- data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
- data/lib/samagotchi/message_parts.rb +3 -2
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/session_config.rb +5 -4
- data/lib/samagotchi/system_one/record.rb +10 -2
- data/lib/samagotchi/toolkit.rb +8 -5
- data/lib/samagotchi/toolkit_binding.rb +30 -5
- data/lib/samagotchi/toolkit_report_command.rb +9 -5
- data/lib/samagotchi/toolkit_suggest.rb +12 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -10
- data/lib/samagotchi/tools/deferred_tools.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +20 -2
- data/lib/samagotchi/tools/registry.rb +44 -20
- data/lib/samagotchi/tools/tool_index.rb +27 -21
- data/lib/samagotchi/update_command.rb +13 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -1
- data/lib/samagotchi/web/public/app.js +61 -357
- data/lib/samagotchi/web/public/composer_mode.js +110 -0
- data/lib/samagotchi/web/public/owner_claim.js +81 -0
- data/lib/samagotchi/web/public/owner_verify.js +112 -0
- data/lib/samagotchi/web/public/session_fields.js +3 -3
- data/lib/samagotchi/web/public/strip_grip.js +3 -3
- data/lib/samagotchi/web/public/strip_hidden.js +65 -0
- data/lib/samagotchi/web/public/version_notices.js +132 -0
- data/lib/samagotchi/web/session_access_routes.rb +2 -1
- data/lib/samagotchi/web/session_hub.rb +6 -0
- data/lib/samagotchi/worker.rb +6 -0
- metadata +11 -1
|
@@ -16,11 +16,14 @@ module Samagotchi
|
|
|
16
16
|
Install a memory bundle from a directory, zip, tar archive, or git URL.
|
|
17
17
|
Source is the path/URL to the bundle (dir/zip/tar.gz/git).
|
|
18
18
|
--force overwrites existing entries (one that differs is copied to the trash first); default skips them.
|
|
19
|
+
A bundle whose requires_chi this chi doesn't meet is refused; --force installs it anyway.
|
|
19
20
|
|
|
20
|
-
upgrade <source> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]
|
|
21
|
+
upgrade <source|name> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]
|
|
21
22
|
Upgrade a bundle (3-way: a file you didn't edit updates; one you edited takes a text
|
|
22
23
|
merge, the old file in the bundle trash; a real conflict leaves it as it is).
|
|
24
|
+
A bare NAME upgrades the bundle installed under that name from the source it came from.
|
|
23
25
|
--dry-run shows what would change. --agent launches interactive session on conflict.
|
|
26
|
+
A bundle whose requires_chi this chi doesn't meet is refused; --force installs it anyway.
|
|
24
27
|
|
|
25
28
|
uninstall <bundle> [--scope system|project] [--force]
|
|
26
29
|
Remove a bundle and its index entries (--force moves a locally edited file to the trash).
|
|
@@ -69,13 +72,19 @@ module Samagotchi
|
|
|
69
72
|
Install a memory bundle from a directory, zip, tar archive, or git URL,
|
|
70
73
|
or a bundle shipped with chi by name (see: chi bundle list).
|
|
71
74
|
--force overwrites existing entries (one that differs is copied to the trash first); default skips them.
|
|
75
|
+
It also installs a bundle whose requires_chi this chi doesn't meet: without --force that install is
|
|
76
|
+
refused, and the bundle is not written anywhere.
|
|
72
77
|
install --dev PATH
|
|
73
78
|
Install the bundle in the local directory PATH as a link to it: its plugin, hooks, rules and scripts
|
|
74
79
|
are PATH's own files (an edit applies at the next worker or chi web restart, with no re-install) and
|
|
75
80
|
their sha256 isn't checked, for this bundle only. Its memories are copied; re-install with --force to
|
|
76
81
|
pick up memory edits. Upgrade refuses it; uninstall removes the link, never PATH's files.
|
|
77
82
|
TEXT
|
|
78
|
-
UPGRADE_HELP =
|
|
83
|
+
UPGRADE_HELP = <<~TEXT
|
|
84
|
+
Usage: chi bundle upgrade <source|name> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]
|
|
85
|
+
--force takes the bundle's file over an edited one, and installs a bundle whose requires_chi this chi
|
|
86
|
+
doesn't meet (without it such a bundle is refused).
|
|
87
|
+
TEXT
|
|
79
88
|
UNINSTALL_HELP = "Usage: chi bundle uninstall <bundle> [--scope system|project] [--force]"
|
|
80
89
|
BUILD_HELP = <<~TEXT
|
|
81
90
|
Usage: chi bundle build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
|
|
@@ -210,7 +219,13 @@ module Samagotchi
|
|
|
210
219
|
@stderr.puts UPGRADE_HELP
|
|
211
220
|
return CLI::Exit::USAGE
|
|
212
221
|
end
|
|
213
|
-
expanded_source =
|
|
222
|
+
expanded_source, record = upgrade_source(source)
|
|
223
|
+
return 1 if expanded_source.nil?
|
|
224
|
+
|
|
225
|
+
# The record's source: said once, so it is clear which bundle and where
|
|
226
|
+
# it comes from when the name alone was given.
|
|
227
|
+
@stdout.puts "Upgrading #{record.name} from #{expanded_source}" if record
|
|
228
|
+
|
|
214
229
|
if Samagotchi::MemoryBundle::Profile.shipped_meta?(expanded_source)
|
|
215
230
|
return install_profile(expanded_source, scope: scope, force: force, dry_run: dry_run, word: "Upgrade")
|
|
216
231
|
end
|
|
@@ -305,7 +320,11 @@ module Samagotchi
|
|
|
305
320
|
|
|
306
321
|
def print_hooks_and_plugin(manifest, plugin: true)
|
|
307
322
|
@stdout.puts "Hooks: #{manifest.hooks.size} hook(s) (#{manifest.hooks.keys.join(", ")})" if manifest&.hooks&.any?
|
|
308
|
-
|
|
323
|
+
return unless plugin && manifest&.plugin
|
|
324
|
+
|
|
325
|
+
# Installed with --force past its requires_chi: the Engine won't load it.
|
|
326
|
+
failure = Samagotchi::MemoryBundle::Manifest.requires_chi_failure(manifest.requires_chi, Samagotchi::VERSION)
|
|
327
|
+
@stdout.puts "Plugin: #{manifest.plugin[:file]} (#{failure ? "won't load on this chi: #{failure}" : "loads at the next chi start"})"
|
|
309
328
|
end
|
|
310
329
|
|
|
311
330
|
# @return [Integer] 0
|
|
@@ -499,9 +518,11 @@ module Samagotchi
|
|
|
499
518
|
label += "; #{info[:merge_note].line}#{" (old copy in #{info[:merge_note].trash})" if info[:merge_note].trash}" if info[:merge_note]
|
|
500
519
|
@stdout.puts " #{k}: #{label}"
|
|
501
520
|
end
|
|
521
|
+
@stdout.puts " #{manifest_line(st.provenance)}"
|
|
502
522
|
# Hooks
|
|
503
|
-
|
|
504
|
-
|
|
523
|
+
manifest = st.provenance.manifest
|
|
524
|
+
hooks = manifest.hooks
|
|
525
|
+
trust = manifest.trust_level || "experimental"
|
|
505
526
|
commit = st.provenance.source_commit
|
|
506
527
|
@stdout.puts " trust_level: #{trust}" if hooks.any? || trust
|
|
507
528
|
@stdout.puts " source_commit: #{commit}" if commit
|
|
@@ -512,7 +533,7 @@ module Samagotchi
|
|
|
512
533
|
exists = File.exist?(hook_path) ? "ok" : "missing"
|
|
513
534
|
@stdout.puts " #{hk}: #{hook.events_field} on_error=#{hook.on_error} priority=#{hook.priority} [#{exists}]"
|
|
514
535
|
end
|
|
515
|
-
@stdout.puts " requires_chi: #{
|
|
536
|
+
@stdout.puts " requires_chi: #{manifest.requires_chi}" if st.hooks_requires_failure
|
|
516
537
|
@stdout.puts " not loaded: #{st.hooks_requires_failure}" if st.hooks_requires_failure
|
|
517
538
|
end
|
|
518
539
|
if (plugin = st.plugin)
|
|
@@ -534,7 +555,7 @@ module Samagotchi
|
|
|
534
555
|
|
|
535
556
|
st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
|
|
536
557
|
mods = st.files.values.count { |v| v[:behind] || v[:missing] || v[:unreadable] }
|
|
537
|
-
hooks_count = st.provenance.hooks.size
|
|
558
|
+
hooks_count = st.provenance.manifest.hooks.size
|
|
538
559
|
hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
|
|
539
560
|
plugin = st.plugin
|
|
540
561
|
mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
|
|
@@ -549,6 +570,14 @@ module Samagotchi
|
|
|
549
570
|
0
|
|
550
571
|
end
|
|
551
572
|
|
|
573
|
+
# "Manifest: [kept]", or "[from the install record]", and what its case
|
|
574
|
+
# says (MemoryBundle::ManifestStanding).
|
|
575
|
+
def manifest_line(record)
|
|
576
|
+
require_relative "memory_bundle/manifest_standing"
|
|
577
|
+
standing = Samagotchi::MemoryBundle::ManifestStanding
|
|
578
|
+
["Manifest:", standing.label(record), standing.status_line(record)].compact.join(" ")
|
|
579
|
+
end
|
|
580
|
+
|
|
552
581
|
# " dev → /path (missing)" when a dev install's source isn't there, else "".
|
|
553
582
|
def dev_label(record)
|
|
554
583
|
return "" unless record.dev
|
|
@@ -577,7 +606,7 @@ module Samagotchi
|
|
|
577
606
|
rescue ArgumentError => e
|
|
578
607
|
@stderr.puts "Bundle '#{bname}': #{e.message}"; return 1
|
|
579
608
|
end
|
|
580
|
-
hooks = record.hooks
|
|
609
|
+
hooks = record.manifest.hooks
|
|
581
610
|
plugin_path = prov.plugin_path(record)
|
|
582
611
|
plugin_file = plugin_path && File.basename(plugin_path)
|
|
583
612
|
if file_arg
|
|
@@ -612,6 +641,26 @@ module Samagotchi
|
|
|
612
641
|
|
|
613
642
|
def diff_memory(prov, target_dir, key)
|
|
614
643
|
print_base_and_current(key, prov.base_path(key), File.join(target_dir, key))
|
|
644
|
+
print_incoming(prov, key)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# The bundle's text a file stays behind (Provenance#incoming_path), so the
|
|
648
|
+
# sections the "stays behind" line names can be read. No saved copy: nothing.
|
|
649
|
+
def print_incoming(prov, key)
|
|
650
|
+
path = prov.incoming_path(key)
|
|
651
|
+
return unless File.exist?(path)
|
|
652
|
+
|
|
653
|
+
text = begin
|
|
654
|
+
File.read(path)
|
|
655
|
+
rescue SystemCallError
|
|
656
|
+
nil
|
|
657
|
+
end
|
|
658
|
+
if text
|
|
659
|
+
@stdout.puts "--- incoming (the bundle's #{prov.record.version}, which this file stays behind) ---"
|
|
660
|
+
@stdout.puts text
|
|
661
|
+
else
|
|
662
|
+
@stdout.puts "--- incoming: the bundle's saved copy can't be read ---"
|
|
663
|
+
end
|
|
615
664
|
end
|
|
616
665
|
|
|
617
666
|
def diff_hook(prov, key, hook)
|
|
@@ -622,7 +671,8 @@ module Samagotchi
|
|
|
622
671
|
def diff_plugin(prov, record, plugin_path)
|
|
623
672
|
file = File.basename(plugin_path)
|
|
624
673
|
print_base_and_current("plugin/#{file}", prov.plugin_base_path(file), plugin_path)
|
|
625
|
-
|
|
674
|
+
requires_chi = record.manifest.requires_chi
|
|
675
|
+
@stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{requires_chi}" if requires_chi}"
|
|
626
676
|
end
|
|
627
677
|
|
|
628
678
|
def print_base_and_current(heading, base, current)
|
|
@@ -926,6 +976,34 @@ module Samagotchi
|
|
|
926
976
|
Samagotchi::MemoryBundle::SourceNormalizer.shipped_bundle_dir(src) || File.expand_path(src)
|
|
927
977
|
end
|
|
928
978
|
|
|
979
|
+
# Where an upgrade comes from: the source as given — a git URL, an
|
|
980
|
+
# existing path, a shipped bundle's name, as install takes them — or,
|
|
981
|
+
# for the bare name of an installed bundle, the source its record keeps.
|
|
982
|
+
# A dev install is refused (it already follows its source), and a record
|
|
983
|
+
# whose source is relative, empty or gone says so. nil after such a
|
|
984
|
+
# refusal, written to stderr.
|
|
985
|
+
# @return [Array(String, InstalledBundle, nil), nil] [the source, the
|
|
986
|
+
# record that named it (nil for a source as given)]
|
|
987
|
+
def upgrade_source(source)
|
|
988
|
+
normalizer = Samagotchi::MemoryBundle::SourceNormalizer
|
|
989
|
+
given = normalizer.git_url?(source) || normalizer.shipped_bundle_dir(source) || File.exist?(File.expand_path(source))
|
|
990
|
+
return [expand_source(source), nil] if given
|
|
991
|
+
|
|
992
|
+
record = Samagotchi::MemoryBundle::Provenance.new(name: source.to_s.strip).record
|
|
993
|
+
return [expand_source(source), nil] if record.nil? || record.error?
|
|
994
|
+
|
|
995
|
+
if record.dev
|
|
996
|
+
@stderr.puts "Upgrade failed: #{Samagotchi::MemoryBundle::Installer.dev_install_line(record.name, record.source)}"
|
|
997
|
+
return nil
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
recorded = record.source.to_s.strip
|
|
1001
|
+
return [recorded, record] if normalizer.git_url?(recorded) || (recorded.start_with?("/") && File.exist?(recorded))
|
|
1002
|
+
|
|
1003
|
+
@stderr.puts "Upgrade failed: #{Samagotchi::MemoryBundle::Installer.source_gone_line(record.name, recorded)}"
|
|
1004
|
+
nil
|
|
1005
|
+
end
|
|
1006
|
+
|
|
929
1007
|
def build_conflict_prompt(bundle_name, conflicts, expanded_source)
|
|
930
1008
|
lines = []
|
|
931
1009
|
lines << "Memory bundle upgrade has conflicts that need interactive resolution."
|
|
@@ -66,13 +66,11 @@ module Samagotchi
|
|
|
66
66
|
# missing need. A manifest.json or needs: that doesn't parse is skipped.
|
|
67
67
|
def self.entry_markers(scope, path:, bundles_dir:)
|
|
68
68
|
MemoryBundle::Provenance.each_installed(dir: bundles_dir).each_with_object({}) do |(_name, bundle), acc|
|
|
69
|
-
next if bundle.error? || bundle.effective_scope != scope.to_s
|
|
69
|
+
next if bundle.error? || bundle.effective_scope != scope.to_s
|
|
70
|
+
|
|
71
|
+
needs = bundle.manifest.needs
|
|
72
|
+
next if needs.empty?
|
|
70
73
|
|
|
71
|
-
needs = begin
|
|
72
|
-
MemoryBundle::Manifest.parse_needs(bundle.needs)
|
|
73
|
-
rescue MemoryBundle::Manifest::ValidationError
|
|
74
|
-
next
|
|
75
|
-
end
|
|
76
74
|
marker = marker(missing(needs, path: path))
|
|
77
75
|
next unless marker
|
|
78
76
|
|
|
@@ -32,7 +32,7 @@ module Samagotchi
|
|
|
32
32
|
next ["config.yml bundles: #{name}: #{SessionConfig.no_installed_bundle(name)}#{near(name, installed.keys)}"] unless bundle
|
|
33
33
|
next [] if bundle.error?
|
|
34
34
|
|
|
35
|
-
record_warnings(bundle, values)
|
|
35
|
+
record_warnings(bundle.manifest, values)
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -62,23 +62,24 @@ module Samagotchi
|
|
|
62
62
|
lines
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# One bundle's +values+ against its
|
|
66
|
-
# while it declares no settings:.
|
|
67
|
-
# @param
|
|
65
|
+
# One bundle's +values+ against its manifest (an installed one's or a
|
|
66
|
+
# source's); none while it declares no settings:.
|
|
67
|
+
# @param manifest [MemoryBundle::InstalledManifest, MemoryBundle::Manifest]
|
|
68
|
+
# name, settings, session_settings, setting_types
|
|
68
69
|
# @return [Array<String>]
|
|
69
|
-
def record_warnings(
|
|
70
|
-
return [] if
|
|
70
|
+
def record_warnings(manifest, values)
|
|
71
|
+
return [] if manifest.settings.nil?
|
|
71
72
|
|
|
72
|
-
values.flat_map { |key, value| value_warnings(
|
|
73
|
+
values.flat_map { |key, value| value_warnings(manifest, key.to_s, value) }
|
|
73
74
|
end
|
|
74
75
|
|
|
75
|
-
def value_warnings(
|
|
76
|
-
where = "config.yml bundles: #{
|
|
77
|
-
known =
|
|
78
|
-
return ["#{where}: #{SessionConfig.takes_keys(
|
|
76
|
+
def value_warnings(manifest, key, value)
|
|
77
|
+
where = "config.yml bundles: #{manifest.name}.#{key}"
|
|
78
|
+
known = manifest.settings | manifest.session_settings
|
|
79
|
+
return ["#{where}: #{SessionConfig.takes_keys(manifest.name, known)}#{near(key, known)}"] unless known.include?(key)
|
|
79
80
|
|
|
80
|
-
takes, what = SessionConfig::TYPES[
|
|
81
|
-
takes && !takes.call(value) ? ["#{where}: #{SessionConfig.wrong_type(
|
|
81
|
+
takes, what = SessionConfig::TYPES[manifest.setting_types[key]]
|
|
82
|
+
takes && !takes.call(value) ? ["#{where}: #{SessionConfig.wrong_type(manifest.name, key, what, value)}"] : []
|
|
82
83
|
end
|
|
83
84
|
private_class_method :value_warnings
|
|
84
85
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agents-md
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.2
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Steers a nested AGENTS.md into the conversation the first time a tool touches a file under its directory"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -8,6 +8,6 @@ plugin:
|
|
|
8
8
|
file: plugin.rb
|
|
9
9
|
sha256: sha256:56527b9d12cb9acd8f57ca00881ab159894ae2fb8def22bce66ac3774bb4187b
|
|
10
10
|
# ctx.prompt_files, read when the plugin loads (0.60.0)
|
|
11
|
-
requires_chi: ">= 0.
|
|
11
|
+
requires_chi: ">= 0.143.0"
|
|
12
12
|
# config.yml bundles: agents-md: (another key warns; an older chi ignores this)
|
|
13
13
|
settings: [tools, max_chars]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: btw
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.4
|
|
4
4
|
scope: system
|
|
5
5
|
description: "/btw <question>: a side answer about the conversation, as a card in every UI, even mid-turn; Keep as session forks it into a child session"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
9
|
sha256: sha256:6497129b06d1c3061c1d7d9d7a9c3dfdbcf790e8f2049addad0c26d24771015f
|
|
10
|
-
requires_chi: ">= 0.
|
|
10
|
+
requires_chi: ">= 0.143.0"
|
|
11
11
|
# config.yml bundles: btw: (another key warns; an older chi ignores this)
|
|
12
12
|
settings: [max_tokens, timeout]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loop-guard
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.10
|
|
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
9
|
sha256: sha256:a4ac7c48a903716b7e1c4fa35348439e90ac97d3ad421378ce7d3ac819229a09
|
|
10
|
-
requires_chi: ">= 0.
|
|
10
|
+
requires_chi: ">= 0.143.0"
|
|
11
11
|
# config.yml bundles: loop-guard: (another key warns; an older chi ignores this)
|
|
12
12
|
settings: [deny_after, stop_after, ignore_tools, mode, thinking]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.7.
|
|
3
|
+
version: 0.7.6
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session; their tools go into chi's tool index: the model searches them with find_tools and calls one with call_tool (<server>/<tool>); /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -9,6 +9,6 @@ plugin:
|
|
|
9
9
|
file: plugin.rb
|
|
10
10
|
sha256: sha256:718e53db877eacd2714e69427c69cfc6c551aaf3417bb760a27dac3331363db5
|
|
11
11
|
# 0.67.0 is the release with chi.tool_source (the core tool index the bundle publishes into).
|
|
12
|
-
requires_chi: ">= 0.
|
|
12
|
+
requires_chi: ">= 0.143.0"
|
|
13
13
|
# config.yml bundles: mcp: (another key warns; an older chi ignores this)
|
|
14
14
|
settings: [servers, timeout, startup_timeout]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skills
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.9
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Skills (skill_<name> memories, the steps of a task done together): /skill save, list, show and diff; older versions kept with a short diff line on every update; a nudge when a followed skill's step fails"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
9
|
sha256: sha256:781d943749d59ade0e24d04ba774449d31267eb8ca435835c36c67a3e523e632
|
|
10
|
-
requires_chi: ">= 0.
|
|
10
|
+
requires_chi: ">= 0.143.0"
|
|
11
11
|
# config.yml bundles: skills: (another key warns; an older chi ignores this)
|
|
12
12
|
settings: [history_keep, nudge]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slow-commands
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.5
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Remembers commands that ran past the execute limit in a project; the next identical execute is denied once a turn with advice to run it with task_create and task_wait; /slow-commands lists and forgets them"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -9,6 +9,6 @@ plugin:
|
|
|
9
9
|
sha256: sha256:e51c099c1552bd0cd1fe46e4fd235a3fe8340693e6ca338eb7ed09d8398a6f0f
|
|
10
10
|
# Execute::HANDED_OVER, read when the plugin loads (0.59.0); after_tool_call's
|
|
11
11
|
# call:, elapsed_ms: and result: (0.57.0)
|
|
12
|
-
requires_chi: ">= 0.
|
|
12
|
+
requires_chi: ">= 0.143.0"
|
|
13
13
|
# config.yml bundles: slow-commands: (another key warns; an older chi ignores this)
|
|
14
14
|
settings: [mode, warn_ratio, ttl_days]
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.153.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
8
|
self_map.md: sha256:e695f78f858c29ccfef2ed6f9d6dd86d0ec067ed704a5a1956c01e53617948a1
|
|
9
9
|
config_modification_protocol.md: sha256:8896d1d364ce835040f69b504518d43dbdf959b93fdd9b10f87a8de61cae84a8
|
|
10
|
-
memory_guide.md: sha256:
|
|
10
|
+
memory_guide.md: sha256:8b7727b939b9c04486ca59859561023dcc319a73e0f5b47b3fcf5741edaa69e2
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -113,8 +113,8 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
|
|
|
113
113
|
|
|
114
114
|
| Command | Purpose |
|
|
115
115
|
|---------|---------|
|
|
116
|
-
| `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips them; a skipped file stays the user's (not recorded, never upgraded or removed by the bundle). Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
|
|
117
|
-
| `upgrade <source> [--force] [--dry-run] [--agent]` | 3-way merge upgrade (base vs current vs incoming) per `Merger::Upgrade#outcome` (`Merger.classify`, then a text merge where both sides changed): `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `merged` (both edited, a text merge with `git` comes out clean → the file takes it, the old one goes to the bundle trash, and entries both sides added under a `Changelog` heading are kept), `conflict` (both edited, a real conflict, or no `git` or base → the file stays untouched and marked, reported as `X stays behind 0.8.0: …` with why and what it missed, needs a retry, `--force`, which copies it to the trash first, or interactive `memory_write` resolution). A marked file is retried at the same version: the system bundle's at each chi start and on `chi update`, another shipped bundle's on `chi update`, any other bundle's on `chi bundle upgrade`. A file marked by an older chi merges normally when its recorded base is a version chi shipped (an installed samagotchi gem still has it); otherwise it merges only if the bundle takes out or rewords no line it still holds, and if it does, it stays behind until resolved by hand, with `--agent` or with `--force`, and the retry can't fix that. Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
|
|
116
|
+
| `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips them; a skipped file stays the user's (not recorded, never upgraded or removed by the bundle). It also installs a bundle whose `requires_chi` this chi doesn't meet: without `--force` that install is refused (`NAME can't be installed: it requires chi …`). Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
|
|
117
|
+
| `upgrade <source\|name> [--force] [--dry-run] [--agent]` | A bare `name` is an installed bundle: chi upgrades it from the source its record keeps (its git URL with the `#ref` it recorded, its folder, or the archive it came from) — a dev install is refused, and a record whose source is gone says so and asks for the path or URL. Otherwise 3-way merge upgrade (base vs current vs incoming) per `Merger::Upgrade#outcome` (`Merger.classify`, then a text merge where both sides changed): `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `merged` (both edited, a text merge with `git` comes out clean → the file takes it, the old one goes to the bundle trash, and entries both sides added under a `Changelog` heading are kept), `conflict` (both edited, a real conflict, or no `git` or base → the file stays untouched and marked, reported as `X stays behind 0.8.0: …` with why and what it missed, needs a retry, `--force`, which copies it to the trash first, or interactive `memory_write` resolution). A marked file is retried at the same version: the system bundle's at each chi start and on `chi update`, another shipped bundle's on `chi update`, any other bundle's on `chi bundle upgrade`. A file marked by an older chi merges normally when its recorded base is a version chi shipped (an installed samagotchi gem still has it); otherwise it merges only if the bundle takes out or rewords no line it still holds, and if it does, it stays behind until resolved by hand, with `--agent` or with `--force`, and the retry can't fix that. Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
|
|
118
118
|
| `uninstall <bundle> [--force]` | Moves the bundle's memory files to `.bundles/.trash/<bundle>-<time>/` (refuses locally edited ones unless `--force`; keeps a file another installed bundle also lists), removes their `index.md` lines, deletes the provenance dir. |
|
|
119
119
|
| `status [<bundle>]` | Provenance + per-file `ok|modified|missing|unreadable|no-index` vs stored checksum and base snapshot, and what stays behind or merged at a chi start; `issues=` counts files behind, missing or unreadable, not edits; a model overlay (`<name>.<key>.md`) has no index line and reads `ok (model overlay)`. |
|
|
120
120
|
| `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
|
|
@@ -146,7 +146,7 @@ chi bundle build --scope system --out updated.zip
|
|
|
146
146
|
|
|
147
147
|
### Provenance internals (for debugging)
|
|
148
148
|
|
|
149
|
-
- Each installed bundle is recorded at `<system dir>/.bundles/<name>/manifest.json` + `bases/<file>.md` snapshots (`Provenance`). Used only for upgrade `Merger` and `status`/`diff`. Build does **not** write provenance.
|
|
149
|
+
- Each installed bundle is recorded at `<system dir>/.bundles/<name>/manifest.json` + `bases/<file>.md` snapshots (`Provenance`), with the bundle's own `manifest.yml` kept beside it (chi reads the manifest from there at load). Used only for upgrade `Merger` and `status`/`diff`. Build does **not** write provenance.
|
|
150
150
|
- A bundle records only the files it wrote. `index.md` updates aren't fatal: a failed one is a warning in the install/uninstall output.
|
|
151
151
|
|
|
152
152
|
## Best practices for the agent
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-search
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "web_search: the model searches the web through your SearXNG (its JSON API) and gets titles, URLs and snippets to read with web_fetch"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
plugin:
|
|
8
|
+
file: plugin.rb
|
|
9
|
+
sha256: sha256:ff79881a370a10dfd1762b5fecb02833dd30c8c5e1f8578e3f1f1a214d6f78a5
|
|
10
|
+
# settings: is new in chi 0.143.0 (ctx.http in 0.80.0)
|
|
11
|
+
requires_chi: ">= 0.143.0"
|
|
12
|
+
# config.yml bundles: web-search: (another key warns; an older chi ignores this)
|
|
13
|
+
settings: {url: string, max_results: number, timeout: number, language: string, safesearch: number, categories: string, engines: string}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The web-search bundle (docs/bundles.md, The web-search bundle): one tool,
|
|
4
|
+
# web_search, that asks a SearXNG instance through its JSON API and returns
|
|
5
|
+
# a short list of results (title, URL, snippet). The model reads the ones it
|
|
6
|
+
# wants with web_fetch. The query goes to that SearXNG and on to the engines
|
|
7
|
+
# it is configured with.
|
|
8
|
+
#
|
|
9
|
+
# Settings (config.yml, bundles: web-search:), read at each call from
|
|
10
|
+
# ctx.settings, never at load:
|
|
11
|
+
# url: http://127.0.0.1:8888 the SearXNG root (http/https with a host)
|
|
12
|
+
# max_results: 8 results listed, 1..20
|
|
13
|
+
# timeout: 10 seconds, ctx.http's per-read timeout
|
|
14
|
+
# language: en optional, sent only when set
|
|
15
|
+
# safesearch: 0 optional, 0/1/2
|
|
16
|
+
# categories: general optional, comma-separated
|
|
17
|
+
# engines: duckduckgo,brave optional, comma-separated (SearXNG drops
|
|
18
|
+
# names it doesn't know)
|
|
19
|
+
#
|
|
20
|
+
# SearXNG needs `json` in search.formats (else it answers 403) and, for a
|
|
21
|
+
# local instance, server.limiter: false (else it may answer 429).
|
|
22
|
+
require "json"
|
|
23
|
+
require "uri"
|
|
24
|
+
|
|
25
|
+
class Plugin
|
|
26
|
+
DEFAULT_URL = "http://127.0.0.1:8888"
|
|
27
|
+
DEFAULT_MAX = 8
|
|
28
|
+
MAX_RESULTS = 20
|
|
29
|
+
DEFAULT_TIMEOUT = 10
|
|
30
|
+
SNIPPET_CHARS = 300
|
|
31
|
+
TIME_RANGES = %w[day week month year].freeze
|
|
32
|
+
# Optional settings sent as query parameters, in this order, only when set.
|
|
33
|
+
OPTIONAL = %w[language safesearch categories engines].freeze
|
|
34
|
+
|
|
35
|
+
DESCRIPTION = "Search the web (through the user's SearXNG) and return a numbered list of results: title, URL and " \
|
|
36
|
+
"a short snippet. Snippets are partial and may be stale: when the details matter, read a result's " \
|
|
37
|
+
"URL with web_fetch before answering instead of answering from snippets alone. Use page for more " \
|
|
38
|
+
"results and time_range for recent ones."
|
|
39
|
+
|
|
40
|
+
# A setting that can't be used: the tool's answer, before any request.
|
|
41
|
+
class SettingError < StandardError; end
|
|
42
|
+
|
|
43
|
+
def initialize(_settings); end
|
|
44
|
+
|
|
45
|
+
def register(chi)
|
|
46
|
+
chi.tool "web_search", DESCRIPTION,
|
|
47
|
+
params: {
|
|
48
|
+
query: { type: "string", description: "The search terms", required: true },
|
|
49
|
+
time_range: { type: "string", enum: TIME_RANGES, description: "Only results from the last day, week, month or year" },
|
|
50
|
+
page: { type: "integer", description: "The results page, 1 for the first (the default)" }
|
|
51
|
+
},
|
|
52
|
+
label: "searching", preview: ->(args) { args["query"].to_s } do |args, ctx|
|
|
53
|
+
search(args, ctx)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def search(args, ctx)
|
|
60
|
+
query = args["query"].to_s.strip
|
|
61
|
+
return "Error: query is required" if query.empty?
|
|
62
|
+
|
|
63
|
+
settings = ctx.settings
|
|
64
|
+
root = root_url(settings["url"])
|
|
65
|
+
page = page_of(args["page"])
|
|
66
|
+
res = ctx.http.get("#{root}/search?#{URI.encode_www_form(pairs(query, page, args["time_range"], settings))}",
|
|
67
|
+
timeout: timeout_of(settings["timeout"]))
|
|
68
|
+
answer(res, query, page, max_of(settings["max_results"]))
|
|
69
|
+
rescue SettingError => e
|
|
70
|
+
"Error: #{e.message}"
|
|
71
|
+
rescue Samagotchi::Plugin::HttpCancelled
|
|
72
|
+
"Error: search cancelled"
|
|
73
|
+
rescue Samagotchi::Plugin::HttpError => e
|
|
74
|
+
"Error: SearXNG is not reachable: #{e.message}. Is it running? (bundles: web-search: url)"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def answer(res, query, page, max)
|
|
78
|
+
return status_error(res.status) unless res.ok?
|
|
79
|
+
|
|
80
|
+
data = begin
|
|
81
|
+
JSON.parse(res.body.scrub)
|
|
82
|
+
rescue JSON::ParserError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
return "Error: SearXNG's answer isn't JSON (is url the SearXNG root?)" unless data.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
listing(data, query, page, max)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def status_error(status)
|
|
91
|
+
case status
|
|
92
|
+
when 403 then "Error: SearXNG refused the JSON format (403): add json to search.formats in its settings.yml"
|
|
93
|
+
when 429 then "Error: SearXNG rate-limited the request (429): turn its limiter off for a local instance (server.limiter: false)"
|
|
94
|
+
else "Error: SearXNG answered #{status}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def listing(data, query, page, max)
|
|
99
|
+
results = Array(data["results"]).select { |entry| entry.is_a?(Hash) && !squash(entry["url"]).empty? }.first(max)
|
|
100
|
+
if results.empty?
|
|
101
|
+
failed = Array(data["unresponsive_engines"]).map { |pair| squash(Array(pair).first) }.reject(&:empty?)
|
|
102
|
+
return [%(No results for "#{query}".), (failed.empty? ? nil : "Engines that failed: #{failed.join(", ")}")].compact.join("\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
lines = [%(SearXNG results for "#{query}" (page #{page}):)]
|
|
106
|
+
if (first = first_answer(data["answers"]))
|
|
107
|
+
lines << "Answer: #{first}"
|
|
108
|
+
end
|
|
109
|
+
results.each_with_index { |entry, index| lines.concat(result_lines(entry, index + 1)) }
|
|
110
|
+
suggestions = Array(data["suggestions"]).map { |text| squash(text) }.reject(&:empty?)
|
|
111
|
+
lines << "Suggestions: #{suggestions.join(", ")}" unless suggestions.empty?
|
|
112
|
+
lines.join("\n")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def result_lines(entry, number)
|
|
116
|
+
url = squash(entry["url"])
|
|
117
|
+
title = squash(entry["title"])
|
|
118
|
+
snippet = cut(squash(entry["content"]))
|
|
119
|
+
["#{number}. #{title.empty? ? url : title}", " #{url}", (snippet.empty? ? nil : " #{snippet}")].compact
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# answers: Strings (older SearXNG) or Hashes with the text under "answer".
|
|
123
|
+
def first_answer(answers)
|
|
124
|
+
Array(answers).each do |item|
|
|
125
|
+
text = cut(squash(item.is_a?(Hash) ? item["answer"] : item))
|
|
126
|
+
return text unless text.empty?
|
|
127
|
+
end
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def pairs(query, page, time_range, settings)
|
|
132
|
+
list = [["q", query], %w[format json], ["pageno", page.to_s]]
|
|
133
|
+
list << ["time_range", time_range.to_s] if TIME_RANGES.include?(time_range.to_s)
|
|
134
|
+
OPTIONAL.each do |key|
|
|
135
|
+
value = settings[key]
|
|
136
|
+
list << [key, value.to_s] unless value.nil? || value.to_s.strip.empty?
|
|
137
|
+
end
|
|
138
|
+
list
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def root_url(value)
|
|
142
|
+
url = (value.nil? ? DEFAULT_URL : value.to_s).strip
|
|
143
|
+
uri = URI.parse(url)
|
|
144
|
+
raise SettingError, "bundles: web-search: url must be an http(s) URL" unless %w[http https].include?(uri.scheme) && !uri.host.to_s.empty?
|
|
145
|
+
|
|
146
|
+
url.chomp("/")
|
|
147
|
+
rescue URI::InvalidURIError
|
|
148
|
+
raise SettingError, "bundles: web-search: url must be an http(s) URL"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def page_of(value)
|
|
152
|
+
page = begin
|
|
153
|
+
Integer(value)
|
|
154
|
+
rescue ArgumentError, TypeError, FloatDomainError
|
|
155
|
+
1
|
|
156
|
+
end
|
|
157
|
+
[page, 1].max
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def timeout_of(value) = value.is_a?(Numeric) && value.positive? ? value : DEFAULT_TIMEOUT
|
|
161
|
+
|
|
162
|
+
def max_of(value)
|
|
163
|
+
max = begin
|
|
164
|
+
Integer(value)
|
|
165
|
+
rescue ArgumentError, TypeError, FloatDomainError
|
|
166
|
+
DEFAULT_MAX
|
|
167
|
+
end
|
|
168
|
+
max.clamp(1, MAX_RESULTS)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def squash(value) = value.to_s.scrub.gsub(/\s+/, " ").strip
|
|
172
|
+
|
|
173
|
+
def cut(text) = text.length > SNIPPET_CHARS ? "#{text[0, SNIPPET_CHARS - 1]}…" : text
|
|
174
|
+
end
|
data/lib/samagotchi/child_env.rb
CHANGED
|
@@ -48,6 +48,21 @@ module Samagotchi
|
|
|
48
48
|
# @return [Hash]
|
|
49
49
|
def scrub(env) = env.except(*spawner_keys(env))
|
|
50
50
|
|
|
51
|
+
# +env+ as a spawn argument that really removes what it leaves out. A Hash
|
|
52
|
+
# given to Process.spawn, Open3 or exec is merged onto the spawner's
|
|
53
|
+
# environment: a key only deleted from it is still inherited by the child.
|
|
54
|
+
# Every key +spawner+ has and +env+ doesn't is mapped to nil, which
|
|
55
|
+
# Process.spawn unsets, so the child sees exactly +env+ (and #scrub is
|
|
56
|
+
# safe to use with it). +env+ is not changed.
|
|
57
|
+
# @param env [Hash]
|
|
58
|
+
# @param spawner [Hash] the spawner's environment (default: ENV)
|
|
59
|
+
# @return [Hash]
|
|
60
|
+
def spawn_env(env, spawner: ENV)
|
|
61
|
+
out = env.dup
|
|
62
|
+
spawner.each_key { |key| out[key] = nil unless out.key?(key) }
|
|
63
|
+
out
|
|
64
|
+
end
|
|
65
|
+
|
|
51
66
|
# In gem mode, what Bundler changed in +env+ mapped back
|
|
52
67
|
# (InstalledGem.unbundled_env_changes): a child activates the installed
|
|
53
68
|
# gem itself rather than inherit this app's bundle. {} in a checkout.
|