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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/docs/bundle-authoring.md +5 -5
  4. data/docs/bundles.md +84 -0
  5. data/docs/cli.md +35 -10
  6. data/docs/configuration.md +19 -14
  7. data/docs/guardrails.md +11 -5
  8. data/docs/hooks.md +4 -3
  9. data/docs/internals/anthropic.md +3 -3
  10. data/docs/internals/attached-context.md +3 -0
  11. data/docs/internals/block-renderers.md +3 -1
  12. data/docs/internals/bundles.md +48 -14
  13. data/docs/internals/engine.md +9 -5
  14. data/docs/internals/objectives.md +6 -4
  15. data/docs/internals/participants.md +1 -1
  16. data/docs/internals/prompt-caching.md +7 -3
  17. data/docs/internals/web-frontend.md +20 -17
  18. data/docs/memory.md +6 -1
  19. data/docs/plugins.md +80 -29
  20. data/docs/releasing.md +8 -3
  21. data/docs/sessions.md +2 -2
  22. data/docs/testing.md +2 -2
  23. data/lib/samagotchi/block_renderers.rb +9 -8
  24. data/lib/samagotchi/bundle_command/check.rb +43 -3
  25. data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
  26. data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
  27. data/lib/samagotchi/bundle_command.rb +88 -10
  28. data/lib/samagotchi/bundle_needs.rb +4 -6
  29. data/lib/samagotchi/bundle_settings_check.rb +14 -13
  30. data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
  31. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
  33. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  34. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  35. data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
  36. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  37. data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
  38. data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
  39. data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
  40. data/lib/samagotchi/child_env.rb +15 -0
  41. data/lib/samagotchi/context_providers.rb +42 -10
  42. data/lib/samagotchi/engine.rb +37 -9
  43. data/lib/samagotchi/extension_load.rb +4 -3
  44. data/lib/samagotchi/guardrail_wiring.rb +28 -19
  45. data/lib/samagotchi/guardrails/approval.rb +2 -2
  46. data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
  47. data/lib/samagotchi/guardrails/rules.rb +1 -0
  48. data/lib/samagotchi/guardrails/verdict.rb +20 -0
  49. data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
  50. data/lib/samagotchi/llm_context_stale.rb +2 -2
  51. data/lib/samagotchi/memory_bundle/asset_installer.rb +11 -8
  52. data/lib/samagotchi/memory_bundle/builder.rb +36 -30
  53. data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
  54. data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
  55. data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
  56. data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -46
  57. data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -15
  58. data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
  59. data/lib/samagotchi/memory_bundle/installer.rb +90 -16
  60. data/lib/samagotchi/memory_bundle/manifest.rb +146 -6
  61. data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
  62. data/lib/samagotchi/memory_bundle/merger.rb +7 -8
  63. data/lib/samagotchi/memory_bundle/profile.rb +3 -2
  64. data/lib/samagotchi/memory_bundle/provenance.rb +217 -18
  65. data/lib/samagotchi/memory_bundle/shas.rb +4 -3
  66. data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
  67. data/lib/samagotchi/memory_bundle/status.rb +11 -10
  68. data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
  69. data/lib/samagotchi/message_parts.rb +3 -2
  70. data/lib/samagotchi/plugin/loader.rb +1 -1
  71. data/lib/samagotchi/session_config.rb +5 -4
  72. data/lib/samagotchi/system_one/record.rb +10 -2
  73. data/lib/samagotchi/toolkit.rb +8 -5
  74. data/lib/samagotchi/toolkit_binding.rb +30 -5
  75. data/lib/samagotchi/toolkit_report_command.rb +9 -5
  76. data/lib/samagotchi/toolkit_suggest.rb +12 -6
  77. data/lib/samagotchi/tools/builtin_calls.rb +14 -10
  78. data/lib/samagotchi/tools/deferred_tools.rb +2 -2
  79. data/lib/samagotchi/tools/delegate.rb +20 -2
  80. data/lib/samagotchi/tools/registry.rb +44 -20
  81. data/lib/samagotchi/tools/tool_index.rb +27 -21
  82. data/lib/samagotchi/update_command.rb +13 -2
  83. data/lib/samagotchi/version.rb +1 -1
  84. data/lib/samagotchi/web/app.rb +2 -1
  85. data/lib/samagotchi/web/public/app.js +61 -357
  86. data/lib/samagotchi/web/public/composer_mode.js +110 -0
  87. data/lib/samagotchi/web/public/owner_claim.js +81 -0
  88. data/lib/samagotchi/web/public/owner_verify.js +112 -0
  89. data/lib/samagotchi/web/public/session_fields.js +3 -3
  90. data/lib/samagotchi/web/public/strip_grip.js +3 -3
  91. data/lib/samagotchi/web/public/strip_hidden.js +65 -0
  92. data/lib/samagotchi/web/public/version_notices.js +132 -0
  93. data/lib/samagotchi/web/session_access_routes.rb +2 -1
  94. data/lib/samagotchi/web/session_hub.rb +6 -0
  95. data/lib/samagotchi/worker.rb +6 -0
  96. 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 = "Usage: chi bundle upgrade <source> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]"
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 = expand_source(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
- @stdout.puts "Plugin: #{manifest.plugin[:file]} (loads at the next chi start)" if plugin && manifest&.plugin
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
- hooks = st.provenance.hooks
504
- trust = st.provenance.trust_level || "experimental"
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: #{st.provenance.requires_chi}" if st.hooks_requires_failure
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
- @stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{record.requires_chi}" if record.requires_chi}"
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 || !bundle.needs?
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 record (or a manifest as one); none
66
- # while it declares no settings:.
67
- # @param bundle [MemoryBundle::InstalledBundle]
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(bundle, values)
70
- return [] if bundle.settings.nil?
70
+ def record_warnings(manifest, values)
71
+ return [] if manifest.settings.nil?
71
72
 
72
- values.flat_map { |key, value| value_warnings(bundle, key.to_s, value) }
73
+ values.flat_map { |key, value| value_warnings(manifest, key.to_s, value) }
73
74
  end
74
75
 
75
- def value_warnings(bundle, key, value)
76
- where = "config.yml bundles: #{bundle.name}.#{key}"
77
- known = bundle.settings | bundle.session_settings
78
- return ["#{where}: #{SessionConfig.takes_keys(bundle.name, known)}#{near(key, known)}"] unless known.include?(key)
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[bundle.setting_types[key]]
81
- takes && !takes.call(value) ? ["#{where}: #{SessionConfig.wrong_type(bundle.name, key, what, value)}"] : []
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.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.60.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
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.1.30"
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.9
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.6.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.5
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.121.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.8
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.11.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.4
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.59.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.151.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:bfe9f13e8a6c929056fe531c4c74c7e05d0f17f025191c1a111c8e7dfd224fd6
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
@@ -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.