samagotchi 0.22.0 → 0.23.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 +33 -1
- data/bin/chi +9 -4
- data/docs/configuration.md +4 -4
- data/docs/releasing.md +1 -1
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +4 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +2 -2
- data/lib/samagotchi/bridge/bounded_queue.rb +0 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -6
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -2
- data/lib/samagotchi/bridge.rb +12 -9
- data/lib/samagotchi/bridge_client/event_stream.rb +2 -2
- data/lib/samagotchi/bridge_client.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +39 -29
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +2 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +2 -0
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +6 -5
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +2 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +5 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -7
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cancellation_controller.rb +1 -1
- data/lib/samagotchi/cli/command.rb +1 -1
- data/lib/samagotchi/client.rb +3 -5
- data/lib/samagotchi/commands/registry.rb +2 -2
- data/lib/samagotchi/config.rb +38 -14
- data/lib/samagotchi/context_note.rb +1 -1
- data/lib/samagotchi/context_status.rb +6 -6
- data/lib/samagotchi/continue_offer.rb +15 -8
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/desktop/macos.rb +5 -4
- data/lib/samagotchi/empty_answer_retry.rb +1 -1
- data/lib/samagotchi/engine.rb +17 -17
- data/lib/samagotchi/gem_update.rb +2 -1
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +5 -5
- data/lib/samagotchi/guardrails/approvals.rb +2 -2
- data/lib/samagotchi/guardrails/gate.rb +2 -2
- data/lib/samagotchi/guardrails/read_only_shell.rb +5 -3
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +2 -2
- data/lib/samagotchi/guardrails/shell_paths.rb +2 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -6
- data/lib/samagotchi/hooks/loader.rb +18 -16
- data/lib/samagotchi/hooks/registry.rb +10 -2
- data/lib/samagotchi/host_registry.rb +1 -3
- data/lib/samagotchi/idle_recap.rb +18 -15
- data/lib/samagotchi/idle_reminders.rb +6 -3
- data/lib/samagotchi/image_store.rb +5 -2
- data/lib/samagotchi/kernel_loop.rb +31 -31
- data/lib/samagotchi/llm/chat_loop.rb +9 -6
- data/lib/samagotchi/llm/errors.rb +2 -2
- data/lib/samagotchi/llm/http.rb +12 -12
- data/lib/samagotchi/llm/openai_chat.rb +39 -28
- data/lib/samagotchi/llm/prompt_cache.rb +1 -1
- data/lib/samagotchi/log.rb +2 -2
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/memory_bundle/builder.rb +9 -3
- data/lib/samagotchi/memory_bundle/index_updater.rb +13 -1
- data/lib/samagotchi/memory_bundle/installer.rb +67 -57
- data/lib/samagotchi/memory_bundle/listing.rb +5 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +6 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +16 -13
- data/lib/samagotchi/memory_bundle/source.rb +7 -3
- data/lib/samagotchi/memory_bundle/status.rb +3 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +3 -2
- data/lib/samagotchi/memory_bundle/trash.rb +5 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +9 -6
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/model_overlay.rb +3 -5
- data/lib/samagotchi/model_profile.rb +12 -8
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/output_formatter.rb +18 -17
- data/lib/samagotchi/owner_lock.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +8 -5
- data/lib/samagotchi/plugin/context.rb +1 -3
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/plugin/service.rb +1 -1
- data/lib/samagotchi/plugin/sessions.rb +1 -0
- data/lib/samagotchi/plugin_tasks.rb +1 -1
- data/lib/samagotchi/prompt.rb +68 -12
- data/lib/samagotchi/prompt_literal_guard.rb +1 -1
- data/lib/samagotchi/question_desk.rb +7 -7
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/relay_desk.rb +1 -1
- data/lib/samagotchi/reminder_store.rb +2 -1
- data/lib/samagotchi/send_command.rb +4 -2
- data/lib/samagotchi/session.rb +2 -2
- data/lib/samagotchi/session_archive_command.rb +5 -1
- data/lib/samagotchi/session_commands.rb +1 -3
- data/lib/samagotchi/session_manager.rb +2 -4
- data/lib/samagotchi/session_metrics.rb +10 -10
- data/lib/samagotchi/session_observer.rb +3 -2
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/sessions_command.rb +14 -5
- data/lib/samagotchi/system_prompt.rb +19 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +1 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +10 -10
- data/lib/samagotchi/terminal_ui/input_support.rb +2 -2
- data/lib/samagotchi/terminal_ui/line_reader.rb +3 -3
- data/lib/samagotchi/terminal_ui/question_prompt.rb +23 -23
- data/lib/samagotchi/terminal_ui/question_slot.rb +10 -10
- data/lib/samagotchi/terminal_ui/reline_seam.rb +1 -1
- data/lib/samagotchi/terminal_ui/repl_input.rb +3 -2
- data/lib/samagotchi/terminal_ui.rb +9 -5
- data/lib/samagotchi/text_diff.rb +12 -12
- data/lib/samagotchi/thinking.rb +6 -2
- data/lib/samagotchi/thought_stream_splitter.rb +6 -3
- data/lib/samagotchi/tool_activity.rb +1 -0
- data/lib/samagotchi/tool_call_parser.rb +13 -14
- data/lib/samagotchi/tool_declarations.rb +36 -35
- data/lib/samagotchi/tools/ask_user_question.rb +3 -4
- data/lib/samagotchi/tools/builtin_calls.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +0 -1
- data/lib/samagotchi/tools/edit.rb +14 -13
- data/lib/samagotchi/tools/execute.rb +3 -5
- data/lib/samagotchi/tools/list_reminders.rb +1 -1
- data/lib/samagotchi/tools/memory.rb +21 -22
- data/lib/samagotchi/tools/read.rb +7 -7
- data/lib/samagotchi/tools/task_create.rb +1 -1
- data/lib/samagotchi/tools/task_get.rb +1 -1
- data/lib/samagotchi/tools/task_list.rb +1 -1
- data/lib/samagotchi/tools/task_runtime.rb +8 -9
- data/lib/samagotchi/tools/task_stop.rb +1 -1
- data/lib/samagotchi/tools/task_wait.rb +3 -4
- data/lib/samagotchi/tools/web_fetch.rb +2 -4
- data/lib/samagotchi/tools/write.rb +1 -1
- data/lib/samagotchi/turn_flow.rb +8 -8
- data/lib/samagotchi/turn_note.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +2 -2
- data/lib/samagotchi/web/app.rb +13 -6
- data/lib/samagotchi/web/markdown_renderer.rb +2 -1
- data/lib/samagotchi/web/qr.rb +7 -1
- data/lib/samagotchi/web/server.rb +6 -2
- data/lib/samagotchi/worker.rb +10 -2
- metadata +20 -20
data/lib/samagotchi/config.rb
CHANGED
|
@@ -4,7 +4,6 @@ require "yaml"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require "uri"
|
|
7
|
-
require "set"
|
|
8
7
|
require_relative "atomic_file"
|
|
9
8
|
require_relative "config_text_edit"
|
|
10
9
|
require_relative "log"
|
|
@@ -56,8 +55,8 @@ module Samagotchi
|
|
|
56
55
|
end
|
|
57
56
|
|
|
58
57
|
# Section names: lower alnum only, no _ or -.
|
|
59
|
-
SECTION_RE = /\A[a-z0-9]+\z
|
|
60
|
-
LEAF_RE = /\A[a-z0-9_]+\z
|
|
58
|
+
SECTION_RE = /\A[a-z0-9]+\z/
|
|
59
|
+
LEAF_RE = /\A[a-z0-9_]+\z/
|
|
61
60
|
|
|
62
61
|
ENTRIES = [
|
|
63
62
|
# universal – env+config+cli
|
|
@@ -126,7 +125,7 @@ module Samagotchi
|
|
|
126
125
|
Entry.new(key: "context.status", yaml_path: %w[context status], type: :bool, default: true, expose: %i[env config cli]),
|
|
127
126
|
Entry.new(key: "context.window_tokens", yaml_path: %w[context window_tokens], type: :integer, default: nil, expose: %i[env config cli]),
|
|
128
127
|
Entry.new(key: "context.chars_per_token", yaml_path: %w[context chars_per_token], type: :float, default: 4.0, expose: %i[env config cli]),
|
|
129
|
-
Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds],type: :string, default: "20,40,60,80", expose: %i[env config cli]),
|
|
128
|
+
Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds], type: :string, default: "20,40,60,80", expose: %i[env config cli]),
|
|
130
129
|
Entry.new(key: "context.status_cadence", yaml_path: %w[context status_cadence], type: :integer, default: 0, expose: %i[env config cli]),
|
|
131
130
|
|
|
132
131
|
Entry.new(key: "thinking.turn_preamble", yaml_path: %w[thinking turn_preamble], type: :bool, default: true, expose: %i[env config cli]),
|
|
@@ -136,7 +135,7 @@ module Samagotchi
|
|
|
136
135
|
# the flag and the env, above config.yml's value.
|
|
137
136
|
Entry.new(key: "thinking.level", yaml_path: %w[thinking level], type: :string, default: "default", expose: %i[env config]),
|
|
138
137
|
|
|
139
|
-
Entry.new(key: "default.
|
|
138
|
+
Entry.new(key: "default.max_tokens", yaml_path: %w[default max_tokens], type: :integer, default: nil, expose: %i[env config cli]),
|
|
140
139
|
Entry.new(key: "max_tool_output_chars", yaml_path: %w[max_tool_output_chars], type: :integer, default: 10_000, expose: %i[env config cli]),
|
|
141
140
|
|
|
142
141
|
Entry.new(key: "retry.max", yaml_path: %w[retry max], type: :integer, default: 5, expose: %i[env config cli]),
|
|
@@ -186,7 +185,7 @@ module Samagotchi
|
|
|
186
185
|
# env+config only
|
|
187
186
|
Entry.new(key: "default.input", yaml_path: %w[default input], type: :string, default: nil, expose: %i[env config]),
|
|
188
187
|
Entry.new(key: "history.file", yaml_path: %w[history file], type: :string, default: nil, expose: %i[env config]),
|
|
189
|
-
Entry.new(key: "skip_agent_md", yaml_path: %w[skip_agent_md], type: :bool, default: false, expose: %i[env config])
|
|
188
|
+
Entry.new(key: "skip_agent_md", yaml_path: %w[skip_agent_md], type: :bool, default: false, expose: %i[env config])
|
|
190
189
|
].freeze
|
|
191
190
|
|
|
192
191
|
# Top-level maps read by their own code, whose entry names and contents
|
|
@@ -228,11 +227,13 @@ module Samagotchi
|
|
|
228
227
|
# Coercion helpers
|
|
229
228
|
def coerce(entry, raw)
|
|
230
229
|
return nil if raw.nil?
|
|
230
|
+
|
|
231
231
|
# For string, preserve as-is (including trailing spaces like "Please ")
|
|
232
232
|
if entry.type == :string
|
|
233
233
|
# A YAML list for a "|"-separated setting (web.annotate_presets).
|
|
234
234
|
str = raw.is_a?(Array) ? raw.join("|") : raw.to_s
|
|
235
235
|
return nil if str.empty? && entry.type != :string
|
|
236
|
+
|
|
236
237
|
return str
|
|
237
238
|
end
|
|
238
239
|
str = raw.to_s.strip
|
|
@@ -267,7 +268,7 @@ module Samagotchi
|
|
|
267
268
|
lowered = str.downcase
|
|
268
269
|
allowed = entry.enum_values.map(&:downcase)
|
|
269
270
|
unless allowed.include?(lowered)
|
|
270
|
-
Log.warn(:config, "invalid_value", echo: "Warning: invalid value for #{entry.key}: #{raw.inspect} (allowed: #{entry.enum_values.join(
|
|
271
|
+
Log.warn(:config, "invalid_value", echo: "Warning: invalid value for #{entry.key}: #{raw.inspect} (allowed: #{entry.enum_values.join(", ")}) — using default", key: entry.key)
|
|
271
272
|
return entry.default
|
|
272
273
|
end
|
|
273
274
|
# return canonical casing from enum_values
|
|
@@ -296,6 +297,7 @@ module Samagotchi
|
|
|
296
297
|
raw = cli_overrides[entry.key]
|
|
297
298
|
# cli_overrides may already be coerced; detect by type
|
|
298
299
|
return [raw, :cli] if already_coerced?(entry, raw)
|
|
300
|
+
|
|
299
301
|
return [coerce(entry, raw), :cli]
|
|
300
302
|
end
|
|
301
303
|
if cli_overrides.key?(entry.cli_flag)
|
|
@@ -323,7 +325,7 @@ module Samagotchi
|
|
|
323
325
|
|
|
324
326
|
def already_coerced?(entry, val)
|
|
325
327
|
case entry.type
|
|
326
|
-
when :bool then
|
|
328
|
+
when :bool then [true, false].include?(val)
|
|
327
329
|
when :integer then val.is_a?(Integer)
|
|
328
330
|
when :float then val.is_a?(Float) || val.is_a?(Integer)
|
|
329
331
|
when :enum then entry.enum_values.include?(val)
|
|
@@ -343,6 +345,7 @@ module Samagotchi
|
|
|
343
345
|
cur = data
|
|
344
346
|
yaml_path.each_with_index do |seg, idx|
|
|
345
347
|
return nil unless cur.is_a?(Hash)
|
|
348
|
+
|
|
346
349
|
last = idx == yaml_path.size - 1
|
|
347
350
|
if last
|
|
348
351
|
candidates = [seg, seg.to_sym]
|
|
@@ -361,6 +364,7 @@ module Samagotchi
|
|
|
361
364
|
# also try kebab alias for section (should not exist per spec, but be lenient)
|
|
362
365
|
nxt ||= cur[seg.tr("_", "-")] || cur[seg.tr("_", "-").to_sym]
|
|
363
366
|
return nil if nxt.nil?
|
|
367
|
+
|
|
364
368
|
cur = nxt
|
|
365
369
|
end
|
|
366
370
|
end
|
|
@@ -404,6 +408,13 @@ module Samagotchi
|
|
|
404
408
|
get_with_origin(key).first
|
|
405
409
|
end
|
|
406
410
|
|
|
411
|
+
# default.max_tokens: the most tokens one generation may produce, or
|
|
412
|
+
# nil when unset or not positive (the server's own limit).
|
|
413
|
+
def max_tokens
|
|
414
|
+
value = get("default.max_tokens").to_i
|
|
415
|
+
value if value.positive?
|
|
416
|
+
end
|
|
417
|
+
|
|
407
418
|
# A timeout setting (server.open_timeout, server.read_timeout) in
|
|
408
419
|
# seconds, +given+ (a caller's own value) first; one that isn't
|
|
409
420
|
# positive (0, a word) is the key's default, on every kind of host.
|
|
@@ -419,6 +430,7 @@ module Samagotchi
|
|
|
419
430
|
def get_with_origin(key)
|
|
420
431
|
entry = find_by_key(key)
|
|
421
432
|
raise ArgumentError, "unknown config key: #{key}" unless entry
|
|
433
|
+
|
|
422
434
|
# Live resolve so ENV changes (as in specs) are reflected without explicit reload
|
|
423
435
|
# Use current ENV and file (via ConfigFile's cached reader), plus any CLI
|
|
424
436
|
# overrides captured via reload!
|
|
@@ -546,7 +558,7 @@ module Samagotchi
|
|
|
546
558
|
candidates = candidates.uniq
|
|
547
559
|
near = near_names(probe, candidates)
|
|
548
560
|
close = (near + candidates.select { |c| c.split(".").last == probe.split(".").last }).uniq.first(3)
|
|
549
|
-
hint = close.empty? ? "" : " (did you mean #{close.map { |c| "'#{prefix}#{c}'" }.join(
|
|
561
|
+
hint = close.empty? ? "" : " (did you mean #{close.map { |c| "'#{prefix}#{c}'" }.join(" or ")}?)"
|
|
550
562
|
"config: unknown key '#{key}'#{hint}"
|
|
551
563
|
end
|
|
552
564
|
|
|
@@ -582,6 +594,7 @@ module Samagotchi
|
|
|
582
594
|
# parsed Hash or nil (missing file / parse error / non-mapping top level).
|
|
583
595
|
def read_yaml(env: ENV, path: global_path(env: env))
|
|
584
596
|
return nil if path.nil?
|
|
597
|
+
|
|
585
598
|
cache = @yaml_cache ||= {}
|
|
586
599
|
unless File.file?(path)
|
|
587
600
|
cache.delete(path)
|
|
@@ -593,7 +606,7 @@ module Samagotchi
|
|
|
593
606
|
return hit[1] if hit && hit[0] == key
|
|
594
607
|
|
|
595
608
|
data = begin
|
|
596
|
-
YAML.
|
|
609
|
+
YAML.safe_load_file(path, permitted_classes: [], aliases: false)
|
|
597
610
|
rescue StandardError
|
|
598
611
|
nil
|
|
599
612
|
end
|
|
@@ -631,7 +644,7 @@ module Samagotchi
|
|
|
631
644
|
# else warns once and counts as unset.
|
|
632
645
|
def bool_flag(value, where, key)
|
|
633
646
|
return nil if value.nil?
|
|
634
|
-
return value if
|
|
647
|
+
return value if [true, false].include?(value)
|
|
635
648
|
|
|
636
649
|
text = value.to_s.strip.downcase
|
|
637
650
|
return true if text == "true"
|
|
@@ -653,6 +666,7 @@ module Samagotchi
|
|
|
653
666
|
# nil. Reserved keys warn once and are dropped. nil when nothing is left.
|
|
654
667
|
def sampling_map(value, where)
|
|
655
668
|
return nil if value.nil?
|
|
669
|
+
|
|
656
670
|
unless value.is_a?(Hash)
|
|
657
671
|
warn_once "Warning: #{where}: sampling must be a map of request parameters; ignored"
|
|
658
672
|
return nil
|
|
@@ -661,6 +675,7 @@ module Samagotchi
|
|
|
661
675
|
result = value.each_with_object({}) do |(raw_key, raw_value), acc|
|
|
662
676
|
key = raw_key.to_s.strip
|
|
663
677
|
next if key.empty?
|
|
678
|
+
|
|
664
679
|
if SAMPLING_RESERVED_KEYS.include?(key)
|
|
665
680
|
warn_once "Warning: #{where}: sampling.#{key} is set by chi; ignored"
|
|
666
681
|
next
|
|
@@ -687,6 +702,7 @@ module Samagotchi
|
|
|
687
702
|
def clear_yaml_cache!(path = nil)
|
|
688
703
|
cache = @yaml_cache
|
|
689
704
|
return if cache.nil?
|
|
705
|
+
|
|
690
706
|
if path
|
|
691
707
|
cache.delete(path)
|
|
692
708
|
else
|
|
@@ -734,6 +750,7 @@ module Samagotchi
|
|
|
734
750
|
raw_hosts.each do |raw_name, raw_cfg|
|
|
735
751
|
name = raw_name.to_s.strip
|
|
736
752
|
next if name.empty?
|
|
753
|
+
|
|
737
754
|
unless name.match?(HOST_NAME_RE)
|
|
738
755
|
warn_once "Warning: ignoring hosts entry '#{name}': must match /[a-z0-9][a-z0-9._-]*/i"
|
|
739
756
|
next
|
|
@@ -795,7 +812,7 @@ module Samagotchi
|
|
|
795
812
|
next
|
|
796
813
|
end
|
|
797
814
|
port_val = port.to_s.strip.empty? ? 8080 : port.to_i
|
|
798
|
-
if port_val <= 0 || port_val >
|
|
815
|
+
if port_val <= 0 || port_val > 65_535
|
|
799
816
|
warn_once "Warning: ignoring hosts entry '#{name}': invalid port"
|
|
800
817
|
next
|
|
801
818
|
end
|
|
@@ -835,7 +852,7 @@ module Samagotchi
|
|
|
835
852
|
default_host = Samagotchi::Config.resolve("server.host", **opts).to_s.strip
|
|
836
853
|
default_host = "localhost" if default_host.empty?
|
|
837
854
|
default_port = Samagotchi::Config.resolve("server.port", **opts).to_i
|
|
838
|
-
default_port = 8080 if default_port <= 0 || default_port >
|
|
855
|
+
default_port = 8080 if default_port <= 0 || default_port > 65_535
|
|
839
856
|
transport_sym = Samagotchi::Config.resolve_with_origin("server.transport", **opts).then do |value, origin|
|
|
840
857
|
origin == :default ? nil : value.to_sym
|
|
841
858
|
end
|
|
@@ -877,7 +894,11 @@ module Samagotchi
|
|
|
877
894
|
|
|
878
895
|
# enabled: false (or "false", any case) in a hosts entry.
|
|
879
896
|
def host_disabled?(cfg)
|
|
880
|
-
enabled = cfg.key?("enabled")
|
|
897
|
+
enabled = if cfg.key?("enabled")
|
|
898
|
+
cfg["enabled"]
|
|
899
|
+
else
|
|
900
|
+
(cfg.key?(:enabled) ? cfg[:enabled] : true)
|
|
901
|
+
end
|
|
881
902
|
enabled == false || enabled.to_s.strip.downcase == "false"
|
|
882
903
|
end
|
|
883
904
|
private_class_method :host_disabled?
|
|
@@ -959,6 +980,7 @@ module Samagotchi
|
|
|
959
980
|
# Only serialize if non-default or explicitly configured hosts:
|
|
960
981
|
# include when hosts file exists with hosts: section or when workers need propagation
|
|
961
982
|
return nil if hosts.nil? || hosts.empty?
|
|
983
|
+
|
|
962
984
|
# Serialize to JSON with string keys
|
|
963
985
|
simple = hosts.transform_values do |v|
|
|
964
986
|
# A url entry travels as its url (host/port come from it); the API
|
|
@@ -990,6 +1012,7 @@ module Samagotchi
|
|
|
990
1012
|
def bundle_settings(env: ENV, path: global_path(env: env))
|
|
991
1013
|
raw = section("bundles", env: env, path: path)
|
|
992
1014
|
return {} if raw.nil?
|
|
1015
|
+
|
|
993
1016
|
unless raw.is_a?(Hash)
|
|
994
1017
|
Log.warn(:hooks, "bundles_section_invalid", echo: "[samagotchi:hooks] config.yml bundles: must be a mapping of bundle name to settings; ignored")
|
|
995
1018
|
return {}
|
|
@@ -1025,6 +1048,7 @@ module Samagotchi
|
|
|
1025
1048
|
raw.each_with_object({}) do |(k, v), result|
|
|
1026
1049
|
key = k.to_s.strip
|
|
1027
1050
|
next if key.empty?
|
|
1051
|
+
|
|
1028
1052
|
val = v.to_s.strip
|
|
1029
1053
|
next if val.empty?
|
|
1030
1054
|
|
|
@@ -42,7 +42,7 @@ module Samagotchi
|
|
|
42
42
|
|
|
43
43
|
# The note's own text, without its frame.
|
|
44
44
|
def text_of(message)
|
|
45
|
-
fetch(message, :content).to_s.sub(/\A\[CONTEXT NOTE[^\n]*\n/, "").
|
|
45
|
+
fetch(message, :content).to_s.sub(/\A\[CONTEXT NOTE[^\n]*\n/, "").delete_suffix("\n[END NOTE]")
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def fetch(message, key)
|
|
@@ -64,7 +64,7 @@ module Samagotchi
|
|
|
64
64
|
def self.last_bucket(conversation)
|
|
65
65
|
message = conversation.reverse.find do |entry|
|
|
66
66
|
content = entry[:content].to_s
|
|
67
|
-
entry[:role] == "system" &&
|
|
67
|
+
entry[:role] == "system" && content.start_with?(STATUS_PREFIX, LINE_PREFIX)
|
|
68
68
|
end
|
|
69
69
|
return nil unless message
|
|
70
70
|
|
|
@@ -221,11 +221,11 @@ module Samagotchi
|
|
|
221
221
|
end
|
|
222
222
|
|
|
223
223
|
def emit?(bucket:, iteration_index:)
|
|
224
|
-
bucket_changed = if @last_bucket.nil?
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
bucket_changed = bucket != if @last_bucket.nil?
|
|
225
|
+
"under#{@thresholds.first}"
|
|
226
|
+
else
|
|
227
|
+
@last_bucket
|
|
228
|
+
end
|
|
229
229
|
cadence_due = @cadence.positive? && ((iteration_index + 1) % @cadence).zero?
|
|
230
230
|
bucket_changed || cadence_due
|
|
231
231
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
|
+
require_relative "log"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
6
7
|
# A session worker's continue offer: what it announces when a turn runs out
|
|
@@ -55,9 +56,9 @@ module Samagotchi
|
|
|
55
56
|
# (or kept, after a cancelled continue turn) is announced and asked.
|
|
56
57
|
def after_turn(result, continue: false, no_interrupt: false)
|
|
57
58
|
outcome = @turn_flow.after_turn(result, continue: continue, no_interrupt: no_interrupt)
|
|
58
|
-
return unless
|
|
59
|
+
return unless %i[continue_offered continue_cancelled].include?(outcome)
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
announce_offer
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
# A prompt (or a reminder turn) taken while a continue is offered
|
|
@@ -111,20 +112,26 @@ module Samagotchi
|
|
|
111
112
|
# The continue offer was answered yes: resume the conversation without a
|
|
112
113
|
# user message, with the iteration limit the offer's turn had. A failure
|
|
113
114
|
# keeps the offer, as the REPL does ("continue prompt preserved"), and
|
|
114
|
-
# asks again
|
|
115
|
-
#
|
|
115
|
+
# asks again, one before the turn began too (logged, no :turn_failed).
|
|
116
|
+
# The answer's text is carried into the turn as it begins; a turn that
|
|
117
|
+
# never began leaves it to no other.
|
|
116
118
|
def run_continue_turn(command)
|
|
117
119
|
offer = @turn_flow.offer
|
|
118
120
|
close("answered")
|
|
119
121
|
steer = @continue_steer
|
|
120
122
|
@continue_steer = nil
|
|
121
123
|
begin
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
begin
|
|
125
|
+
@engine.steer_next_turn(steer[:text], source: steer[:source]) if steer
|
|
126
|
+
@turn_flow.before_continue_turn
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
Log.warn(:worker, "turn_not_begun", continue: true, error: e.class.name, msg: e.message)
|
|
129
|
+
return announce_offer(offer)
|
|
130
|
+
end
|
|
124
131
|
@run_turn.call(nil, continue: true, origin: { client_id: command[:client_id] }.compact,
|
|
125
132
|
max_iterations: @max_iterations.call(offer[:no_interrupt])) do |result, error|
|
|
126
133
|
if error
|
|
127
|
-
|
|
134
|
+
announce_offer(offer)
|
|
128
135
|
else
|
|
129
136
|
after_turn(result, continue: true, no_interrupt: offer[:no_interrupt])
|
|
130
137
|
end
|
|
@@ -145,7 +152,7 @@ module Samagotchi
|
|
|
145
152
|
|
|
146
153
|
private
|
|
147
154
|
|
|
148
|
-
def
|
|
155
|
+
def announce_offer(offer = @turn_flow.offer)
|
|
149
156
|
@engine.announce(type: :continue_offered, context: offer[:context], no_interrupt: offer[:no_interrupt])
|
|
150
157
|
ask
|
|
151
158
|
end
|
data/lib/samagotchi/debug_log.rb
CHANGED
|
@@ -86,7 +86,7 @@ module Samagotchi
|
|
|
86
86
|
|
|
87
87
|
def rotate
|
|
88
88
|
File.open("#{@path}.lock", File::RDWR | File::CREAT, 0o600) do |lock|
|
|
89
|
-
|
|
89
|
+
next unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
90
90
|
|
|
91
91
|
stat = File.stat(@path)
|
|
92
92
|
File.rename(@path, "#{@path}.1") if stat.ino == @ino && stat.size >= @max_bytes
|
|
@@ -22,7 +22,8 @@ module Samagotchi
|
|
|
22
22
|
# command goes through the runner, and the dirs are injectable, so specs
|
|
23
23
|
# (and smokes with register: false) never touch the real system.
|
|
24
24
|
class MacOS
|
|
25
|
-
Error
|
|
25
|
+
class Error < StandardError
|
|
26
|
+
end
|
|
26
27
|
|
|
27
28
|
APP_NAME = "Chi Helper"
|
|
28
29
|
BUNDLE_ID = "dev.samagotchi.chi-helper"
|
|
@@ -166,9 +167,9 @@ module Samagotchi
|
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
# Keeps the login item as it was.
|
|
169
|
-
def upgrade(&
|
|
170
|
+
def upgrade(&)
|
|
170
171
|
was_login = installed? && login == "enabled"
|
|
171
|
-
install(force: true, login: was_login, &
|
|
172
|
+
install(force: true, login: was_login, &)
|
|
172
173
|
end
|
|
173
174
|
|
|
174
175
|
# SMAppService status as the app reports it ("enabled",
|
|
@@ -212,7 +213,7 @@ module Samagotchi
|
|
|
212
213
|
env = launch["env"].is_a?(Hash) ? launch["env"] : {}
|
|
213
214
|
dump, = @runner.run([PBS, "-dump"])
|
|
214
215
|
_, running = @runner.run(["pgrep", "-x", EXECUTABLE])
|
|
215
|
-
hotkey =
|
|
216
|
+
hotkey = JSON.parse(File.read(hotkey_path)) rescue nil
|
|
216
217
|
result.merge(
|
|
217
218
|
app_version: app_version,
|
|
218
219
|
login: login,
|
|
@@ -30,7 +30,7 @@ module Samagotchi
|
|
|
30
30
|
# The request options of a retry generation: the turn's sampling, with
|
|
31
31
|
# TEMPERATURE when it sets none (a configured nil keeps "don't send").
|
|
32
32
|
def self.sampling(base)
|
|
33
|
-
base
|
|
33
|
+
base ||= {}
|
|
34
34
|
base.key?(:temperature) ? base : base.merge(temperature: TEMPERATURE)
|
|
35
35
|
end
|
|
36
36
|
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -557,8 +557,8 @@ module Samagotchi
|
|
|
557
557
|
|
|
558
558
|
# Run the block with the event log held: no event is numbered or
|
|
559
559
|
# delivered meanwhile, and events the block emits keep their order.
|
|
560
|
-
def synchronize_events(&
|
|
561
|
-
@session_observer.synchronize(&
|
|
560
|
+
def synchronize_events(&)
|
|
561
|
+
@session_observer.synchronize(&)
|
|
562
562
|
end
|
|
563
563
|
|
|
564
564
|
# Add a context note to the session's conversation, between turns: a
|
|
@@ -638,8 +638,8 @@ module Samagotchi
|
|
|
638
638
|
# Start an anytime command on its own thread (D8), one #shutdown waits
|
|
639
639
|
# for, so its command_ran is announced before the process leaves.
|
|
640
640
|
# @return [Thread]
|
|
641
|
-
def spawn_anytime(&
|
|
642
|
-
thread = Thread.new(&
|
|
641
|
+
def spawn_anytime(&)
|
|
642
|
+
thread = Thread.new(&)
|
|
643
643
|
@lifecycle_mutex.synchronize do
|
|
644
644
|
@anytime_threads.select!(&:alive?)
|
|
645
645
|
@anytime_threads << thread
|
|
@@ -656,9 +656,9 @@ module Samagotchi
|
|
|
656
656
|
|
|
657
657
|
# Add a plugin's init task (Plugin::Api#init at commit); it starts with
|
|
658
658
|
# #start_init_tasks!.
|
|
659
|
-
def add_init_task(bundle:, label:, plugin_label:, provides_tools:, quiet:, timeout:, failed: nil, &
|
|
659
|
+
def add_init_task(bundle:, label:, plugin_label:, provides_tools:, quiet:, timeout:, failed: nil, &)
|
|
660
660
|
@plugin_tasks.add(bundle: bundle, label: label, plugin_label: plugin_label, provides_tools: provides_tools,
|
|
661
|
-
quiet: quiet, timeout: timeout, failed: failed, &
|
|
661
|
+
quiet: quiet, timeout: timeout, failed: failed, &)
|
|
662
662
|
end
|
|
663
663
|
|
|
664
664
|
# Start the init tasks not started yet, each on its own thread
|
|
@@ -836,8 +836,8 @@ module Samagotchi
|
|
|
836
836
|
# @param name [Symbol] one of the hook names (see Hooks module)
|
|
837
837
|
# @param block [Proc] receives an event hash (may mutate in place)
|
|
838
838
|
# @return [void]
|
|
839
|
-
def register_hook(name, &
|
|
840
|
-
@hooks.register(name, &
|
|
839
|
+
def register_hook(name, &)
|
|
840
|
+
@hooks.register(name, &)
|
|
841
841
|
end
|
|
842
842
|
|
|
843
843
|
# Unregister a previously registered hook.
|
|
@@ -1369,9 +1369,7 @@ module Samagotchi
|
|
|
1369
1369
|
end
|
|
1370
1370
|
|
|
1371
1371
|
# @return [Session] current session (Engine owns create/resume)
|
|
1372
|
-
|
|
1373
|
-
@session
|
|
1374
|
-
end
|
|
1372
|
+
attr_reader :session
|
|
1375
1373
|
|
|
1376
1374
|
# The kernel's Tools::Peers, following the current session. cancelled?
|
|
1377
1375
|
# is the running turn's cancel, for a tool that waits (delegate_result):
|
|
@@ -1424,9 +1422,7 @@ module Samagotchi
|
|
|
1424
1422
|
end
|
|
1425
1423
|
|
|
1426
1424
|
# @return [IdleRecap, nil] the idle recap detector, or nil when disabled
|
|
1427
|
-
|
|
1428
|
-
@recap
|
|
1429
|
-
end
|
|
1425
|
+
attr_reader :recap
|
|
1430
1426
|
|
|
1431
1427
|
# Write the recap now, before the session is left (IdleRecap#write_now).
|
|
1432
1428
|
# @return [String, nil] the recap written, nil when none was (recap off,
|
|
@@ -2128,6 +2124,7 @@ module Samagotchi
|
|
|
2128
2124
|
config_path = Samagotchi::ConfigFile.global_path
|
|
2129
2125
|
data = Samagotchi::ConfigFile.read_yaml(path: config_path)
|
|
2130
2126
|
return Hooks::Loader.load(data, failures: @guardrail_failures) if data.is_a?(Hash)
|
|
2127
|
+
|
|
2131
2128
|
Hooks::Registry.new
|
|
2132
2129
|
end
|
|
2133
2130
|
|
|
@@ -2150,11 +2147,15 @@ module Samagotchi
|
|
|
2150
2147
|
Hooks::BundleLoader.load(bundle_name: bundle_name, hooks_dir: hooks_dir, metadata: data[:hooks], registry: @hooks,
|
|
2151
2148
|
failures: @guardrail_failures, settings: settings[bundle_name.to_s] || {},
|
|
2152
2149
|
requires_chi: data[:requires_chi])
|
|
2153
|
-
rescue Exception => e
|
|
2150
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
|
|
2151
|
+
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
2152
|
+
|
|
2154
2153
|
Log.error(:hooks, "bundle_load_failed", echo: "[samagotchi:hooks] bundle '#{bundle_name}' failed to load hooks: #{e.class}: #{e.message}", bundle: bundle_name, error: e.class.name)
|
|
2155
2154
|
end
|
|
2156
2155
|
end
|
|
2157
|
-
rescue Exception => e
|
|
2156
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
|
|
2157
|
+
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
2158
|
+
|
|
2158
2159
|
Log.error(:hooks, "bundles_load_failed", echo: "[samagotchi:hooks] failed to load bundle hooks: #{e.class}: #{e.message}", error: e.class.name)
|
|
2159
2160
|
end
|
|
2160
2161
|
|
|
@@ -2278,7 +2279,6 @@ module Samagotchi
|
|
|
2278
2279
|
end
|
|
2279
2280
|
private :bundle_settings
|
|
2280
2281
|
|
|
2281
|
-
|
|
2282
2282
|
# The session's current model as a recap target: its host's OpenAI API
|
|
2283
2283
|
# (native llama.cpp hosts serve /v1/chat/completions too), key variable
|
|
2284
2284
|
# and bare model name, as a turn resolves them.
|
|
@@ -15,7 +15,8 @@ module Samagotchi
|
|
|
15
15
|
# SAMAGOTCHI_UPDATE_GEM_FILE=<path to a .gem> (hidden, for smokes) makes that
|
|
16
16
|
# file the newest version and installs it instead.
|
|
17
17
|
class GemUpdate
|
|
18
|
-
Error
|
|
18
|
+
class Error < StandardError
|
|
19
|
+
end
|
|
19
20
|
|
|
20
21
|
LATEST_URL = URI("https://rubygems.org/api/v1/versions/samagotchi/latest.json")
|
|
21
22
|
# Not SAMAGOTCHI_UPDATE_GEM: that is the env form of config update.gem.
|
|
@@ -159,7 +159,7 @@ module Samagotchi
|
|
|
159
159
|
raise Guardrails::Rules::ParseError, "its sha256 differs from the installed one (edited after install? reinstall the bundle)"
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
doc = YAML.
|
|
162
|
+
doc = YAML.safe_load_file(path)
|
|
163
163
|
raise Guardrails::Rules::ParseError, "expected a mapping with rules:" unless doc.is_a?(Hash)
|
|
164
164
|
|
|
165
165
|
loaded.concat(Guardrails::Rules.parse(doc["rules"], source: "bundle #{bundle_name}"))
|
|
@@ -150,13 +150,13 @@ module Samagotchi
|
|
|
150
150
|
return nil unless preview.is_a?(Hash)
|
|
151
151
|
|
|
152
152
|
get = ->(key) { preview[key] || preview[key.to_s] }
|
|
153
|
-
return "would fail: #{get.(:error)}" if get.(:error)
|
|
154
|
-
return "not shown (#{get.(:skipped)})" if get.(:skipped)
|
|
153
|
+
return "would fail: #{get.call(:error)}" if get.call(:error)
|
|
154
|
+
return "not shown (#{get.call(:skipped)})" if get.call(:skipped)
|
|
155
155
|
|
|
156
|
-
added = get.(:added).to_i
|
|
157
|
-
return "new file, #{added} #{added == 1 ? "line" : "lines"}" if get.(:new_file)
|
|
156
|
+
added = get.call(:added).to_i
|
|
157
|
+
return "new file, #{added} #{added == 1 ? "line" : "lines"}" if get.call(:new_file)
|
|
158
158
|
|
|
159
|
-
"+#{added} −#{get.(:removed).to_i}"
|
|
159
|
+
"+#{added} −#{get.call(:removed).to_i}"
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
162
|
end
|
|
@@ -72,8 +72,8 @@ module Samagotchi
|
|
|
72
72
|
list.find do |e|
|
|
73
73
|
case e["scope"]
|
|
74
74
|
when "session" then session_id && e["session_id"] == session_id && e["key"] == key
|
|
75
|
-
when "repo" then here.(e) && e["key"] == key
|
|
76
|
-
when "rule" then verdict.rule && e["rule"] == verdict.rule && e["source"] == verdict.source && here.(e)
|
|
75
|
+
when "repo" then here.call(e) && e["key"] == key
|
|
76
|
+
when "rule" then verdict.rule && e["rule"] == verdict.rule && e["source"] == verdict.source && here.call(e)
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
end
|
|
@@ -120,8 +120,8 @@ module Samagotchi
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# Registry#fire_each rescues a raising hook itself.
|
|
123
|
-
def fire_each(name, event, &
|
|
124
|
-
@hooks_lookup.call&.fire_each(name, event, &
|
|
123
|
+
def fire_each(name, event, &)
|
|
124
|
+
@hooks_lookup.call&.fire_each(name, event, &)
|
|
125
125
|
end
|
|
126
126
|
end
|
|
127
127
|
end
|
|
@@ -167,7 +167,7 @@ module Samagotchi
|
|
|
167
167
|
program = nil
|
|
168
168
|
until rest.empty?
|
|
169
169
|
arg = rest.shift
|
|
170
|
-
if
|
|
170
|
+
if ["-F", "-v"].include?(arg) then rest.shift
|
|
171
171
|
elsif arg.match?(/\A-[Fv]./m) then next
|
|
172
172
|
elsif arg == "--" then program = rest.shift
|
|
173
173
|
elsif arg.start_with?("-") then return false
|
|
@@ -296,14 +296,16 @@ module Samagotchi
|
|
|
296
296
|
delim = @s.getch
|
|
297
297
|
return false if delim.nil? || delim.match?(/[\s\\]/)
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
# Two calls, two fields: each one advances the scanner (pattern, then replacement).
|
|
300
|
+
delimited(delim) && delimited(delim) && @s.skip(/[gpiImM\d]*/) && true # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
|
|
300
301
|
end
|
|
301
302
|
|
|
302
303
|
def transliterate
|
|
303
304
|
delim = @s.getch
|
|
304
305
|
return false if delim.nil? || delim.match?(/[\s\\]/)
|
|
305
306
|
|
|
306
|
-
|
|
307
|
+
# Source set, then target set: each call advances the scanner.
|
|
308
|
+
delimited(delim) && delimited(delim) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
|
|
307
309
|
end
|
|
308
310
|
|
|
309
311
|
# Up to the next unescaped +delim+ (consumed); false at the end.
|
|
@@ -123,7 +123,7 @@ module Samagotchi
|
|
|
123
123
|
def expand(arg, dir)
|
|
124
124
|
return nil if arg.include?(ShellLex::SUBST)
|
|
125
125
|
|
|
126
|
-
arg = arg.sub(
|
|
126
|
+
arg = arg.sub(%r{\A(~|\$HOME|\$\{HOME\})(?=/|\z)}) { @home }
|
|
127
127
|
return nil if arg.include?("$") || arg.start_with?("~")
|
|
128
128
|
return File.expand_path(arg) if arg.start_with?("/")
|
|
129
129
|
|
|
@@ -142,12 +142,12 @@ module Samagotchi
|
|
|
142
142
|
# >, >>, 2>&1, &>, <, <<: kept in the word (the walk drops them).
|
|
143
143
|
def redirection
|
|
144
144
|
special(@s[@i])
|
|
145
|
-
add(@s[@i], 1) while @i < @s.size && @s[@i].match?(/[&\d>-]/) && @word.
|
|
145
|
+
add(@s[@i], 1) while @i < @s.size && @s[@i].match?(/[&\d>-]/) && @word.end_with?(">")
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
def operator_or_char(char)
|
|
149
149
|
op = OPERATORS.find { |o| @s[@i, o.size] == o }
|
|
150
|
-
return special(char) if
|
|
150
|
+
return special(char) if ["{", "}"].include?(char)
|
|
151
151
|
return add(char, 1) unless op
|
|
152
152
|
|
|
153
153
|
if op == "&" && @word.match?(/\d*>\z/)
|
|
@@ -107,7 +107,7 @@ module Samagotchi
|
|
|
107
107
|
def candidate(word)
|
|
108
108
|
word = word.sub(REDIRECT_PREFIX, "")
|
|
109
109
|
word = word.split("=", 2).last if word.match?(/\A(--?[\w-]+|[A-Za-z_]\w*)=/)
|
|
110
|
-
return nil if word.empty? || word == "-" || word.match?(
|
|
110
|
+
return nil if word.empty? || word == "-" || word.match?(%r{\A-[^/]})
|
|
111
111
|
|
|
112
112
|
word
|
|
113
113
|
end
|
|
@@ -124,7 +124,7 @@ module Samagotchi
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
def expand(word)
|
|
127
|
-
word = word.sub(
|
|
127
|
+
word = word.sub(%r{\A~(?=/|\z)}) { @home }
|
|
128
128
|
word = word.sub(EXPANSION) { value(Regexp.last_match(1) || Regexp.last_match(2)) || "$" }
|
|
129
129
|
return nil if word.include?("$") || word.start_with?("~")
|
|
130
130
|
|