samagotchi 0.35.0 → 0.37.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 +57 -1
- data/README.md +2 -2
- data/bin/chi +1 -1
- data/docs/broadcast.md +91 -12
- data/docs/configuration.md +8 -1
- data/docs/context.md +37 -0
- data/docs/guardrails.md +26 -3
- data/docs/hooks.md +1 -1
- data/docs/memory.md +3 -0
- data/docs/plugins.md +51 -4
- data/lib/samagotchi/broadcast/triage.rb +267 -0
- data/lib/samagotchi/broadcast/triage_log.rb +165 -0
- data/lib/samagotchi/broadcast/triage_model.rb +56 -0
- data/lib/samagotchi/broadcast_command.rb +273 -69
- data/lib/samagotchi/bundles/coordinator/manifest.yml +7 -6
- data/lib/samagotchi/bundles/coordinator/plugin.rb +72 -4
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +59 -11
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +444 -3
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +11 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +5 -4
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +72 -27
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +4 -2
- data/lib/samagotchi/cli/command.rb +27 -3
- data/lib/samagotchi/config.rb +9 -0
- data/lib/samagotchi/context_command.rb +0 -17
- data/lib/samagotchi/engine.rb +4 -6
- data/lib/samagotchi/guardrail_wiring.rb +7 -2
- data/lib/samagotchi/guardrails/approval.rb +5 -2
- data/lib/samagotchi/guardrails/approvals.rb +9 -5
- data/lib/samagotchi/guardrails/parent_approvals.rb +6 -0
- data/lib/samagotchi/guardrails/rules.rb +19 -6
- data/lib/samagotchi/guardrails/targets.rb +83 -11
- data/lib/samagotchi/idle_client.rb +48 -28
- data/lib/samagotchi/idle_recap.rb +6 -5
- data/lib/samagotchi/idle_target.rb +95 -0
- data/lib/samagotchi/llm/openai_chat.rb +29 -3
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/memory_bundle/index_updater.rb +6 -3
- data/lib/samagotchi/note_command.rb +0 -21
- data/lib/samagotchi/recap_setup.rb +38 -52
- data/lib/samagotchi/send_command.rb +0 -22
- data/lib/samagotchi/session_commands.rb +1 -0
- data/lib/samagotchi/tool_activity.rb +14 -3
- data/lib/samagotchi/tool_declarations.rb +8 -4
- data/lib/samagotchi/tools/builtins.rb +1 -1
- data/lib/samagotchi/tools/memory.rb +103 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/question_card.js +4 -3
- metadata +5 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "config"
|
|
4
4
|
require_relative "log"
|
|
5
5
|
require_relative "idle_recap"
|
|
6
|
+
require_relative "idle_target"
|
|
6
7
|
require_relative "recap_store"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
@@ -41,60 +42,21 @@ module Samagotchi
|
|
|
41
42
|
base_url = string_config(kwarg_config, :base_url) || registry_string("recap.base_url")
|
|
42
43
|
host_ref = string_config(kwarg_config, :host_ref) || string_config(kwarg_config, :host) || registry_string("recap.host_ref")
|
|
43
44
|
model = string_config(kwarg_config, :model) || registry_string("recap.model")
|
|
44
|
-
label = model
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
target = lambda do
|
|
58
|
-
resolved = host_registry.resolve(model)
|
|
59
|
-
{ base_url: resolved.openai_base_url, api_key_env: resolved.entry.api_key_env,
|
|
60
|
-
model: resolved.bare_model, label: label.to_s.strip }
|
|
61
|
-
end
|
|
62
|
-
else
|
|
63
|
-
host_ref ||= parsed.host_name if parsed && base_url.nil?
|
|
64
|
-
# If host_ref given, derive base_url (the host's OpenAI base) and its
|
|
65
|
-
# API key variable from the host_registry entry
|
|
66
|
-
api_key_env = nil
|
|
67
|
-
if host_ref && !host_ref.empty?
|
|
68
|
-
entry = host_registry.find_entry(host_ref)
|
|
69
|
-
if entry
|
|
70
|
-
base_url = entry.openai_base_url
|
|
71
|
-
api_key_env = entry.api_key_env
|
|
72
|
-
named = parsed&.host_conflict || parsed&.host_name
|
|
73
|
-
if named && named != entry.name
|
|
74
|
-
Log.warn(:recap, "model_host_mismatch",
|
|
75
|
-
echo: "Warning: recap model '#{model}' names host '#{named}', not recap.host_ref '#{host_ref}'; recap disabled.",
|
|
76
|
-
model: model, host_ref: host_ref)
|
|
77
|
-
return nil
|
|
78
|
-
end
|
|
79
|
-
model = parsed.id if parsed
|
|
80
|
-
else
|
|
81
|
-
Log.warn(:recap, "host_ref_unknown", echo: "Warning: recap host_ref '#{host_ref}' not found in hosts:; recap disabled.", host_ref: host_ref)
|
|
82
|
-
return nil
|
|
83
|
-
end
|
|
84
|
-
elsif parsed && !parsed.host_name
|
|
85
|
-
model = parsed.id
|
|
86
|
-
end
|
|
46
|
+
settings = { model: model, host_ref: host_ref, base_url: base_url, host_registry: host_registry }
|
|
47
|
+
target = if base_url.nil? && host_ref.nil? && model.nil?
|
|
48
|
+
-> { session_target.call }
|
|
49
|
+
elsif !IdleTarget.host_named?(**settings)
|
|
50
|
+
# A model naming no host goes where a bare --model goes
|
|
51
|
+
# (HostRegistry#host_for_model: the default host unless
|
|
52
|
+
# another's list has it), resolved at each attempt.
|
|
53
|
+
-> { IdleTarget.resolve(**settings) }
|
|
54
|
+
else
|
|
55
|
+
fixed = fixed_target(settings)
|
|
56
|
+
return nil unless fixed
|
|
87
57
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
echo: "Warning: SAMAGOTCHI session recap is enabled but base_url/model are missing; recap disabled. " \
|
|
91
|
-
"Set recap: {host_ref:, model:} or SAMAGOTCHI_RECAP_BASE_URL and SAMAGOTCHI_RECAP_MODEL (or pass recap: {base_url:, model:}), " \
|
|
92
|
-
"or leave them all out to recap with the session's own model.")
|
|
93
|
-
return nil
|
|
94
|
-
end
|
|
95
|
-
fixed = { base_url: base_url.to_s.strip, api_key_env: api_key_env, model: model.to_s.strip, label: label.to_s.strip }
|
|
96
|
-
target = -> { fixed }
|
|
97
|
-
end
|
|
58
|
+
-> { fixed }
|
|
59
|
+
end
|
|
98
60
|
|
|
99
61
|
IdleRecap.new(
|
|
100
62
|
engine: engine,
|
|
@@ -107,6 +69,30 @@ module Samagotchi
|
|
|
107
69
|
)
|
|
108
70
|
end
|
|
109
71
|
|
|
72
|
+
# The recap's pinned host and model (IdleTarget.resolve), or nil after
|
|
73
|
+
# a warning when they can't be resolved (recap stays off).
|
|
74
|
+
# @return [IdleTarget, nil]
|
|
75
|
+
def self.fixed_target(settings)
|
|
76
|
+
IdleTarget.resolve(**settings)
|
|
77
|
+
rescue IdleTarget::Unresolved => e
|
|
78
|
+
case e.reason
|
|
79
|
+
when :host_ref_unknown
|
|
80
|
+
Log.warn(:recap, "host_ref_unknown", echo: "Warning: recap host_ref '#{e.host_ref}' not found in hosts:; recap disabled.",
|
|
81
|
+
host_ref: e.host_ref)
|
|
82
|
+
when :model_host_mismatch
|
|
83
|
+
Log.warn(:recap, "model_host_mismatch",
|
|
84
|
+
echo: "Warning: recap model '#{e.model}' names host '#{e.named}', not recap.host_ref '#{e.host_ref}'; recap disabled.",
|
|
85
|
+
model: e.model, host_ref: e.host_ref)
|
|
86
|
+
else
|
|
87
|
+
Log.warn(:recap, "recap_unconfigured",
|
|
88
|
+
echo: "Warning: SAMAGOTCHI session recap is enabled but base_url/model are missing; recap disabled. " \
|
|
89
|
+
"Set recap: {host_ref:, model:} or SAMAGOTCHI_RECAP_BASE_URL and SAMAGOTCHI_RECAP_MODEL (or pass recap: {base_url:, model:}), " \
|
|
90
|
+
"or leave them all out to recap with the session's own model.")
|
|
91
|
+
end
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
private_class_method :fixed_target
|
|
95
|
+
|
|
110
96
|
# recap.sentences as [min, max]; an invalid value warns and falls back to
|
|
111
97
|
# the default range (the recap stays on).
|
|
112
98
|
def self.recap_sentences(value)
|
|
@@ -225,21 +225,6 @@ module Samagotchi
|
|
|
225
225
|
"#{ContextQuote.block(context)}#{message}"
|
|
226
226
|
end
|
|
227
227
|
|
|
228
|
-
# Only a pipe or a file is read. A terminal means nobody piped anything
|
|
229
|
-
# in, and a socket a launcher or an agent's shell passes down may never
|
|
230
|
-
# close: with -m, waiting on either would hang a script. (A pipe the
|
|
231
|
-
# caller never closes still hangs, as it would for cat.)
|
|
232
|
-
def read_stdin
|
|
233
|
-
return nil if @stdin.respond_to?(:tty?) && @stdin.tty?
|
|
234
|
-
|
|
235
|
-
if @stdin.respond_to?(:stat)
|
|
236
|
-
stat = @stdin.stat
|
|
237
|
-
return nil unless stat.pipe? || stat.file?
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
@stdin.read
|
|
241
|
-
end
|
|
242
|
-
|
|
243
228
|
# A worker session like the web start page's: saved as running with the
|
|
244
229
|
# message before its worker spawns, so lists and the web show it at
|
|
245
230
|
# once. The full id, so a script can pass it on. The model's host is
|
|
@@ -477,12 +462,5 @@ module Samagotchi
|
|
|
477
462
|
else " with #{@images.size} images"
|
|
478
463
|
end
|
|
479
464
|
end
|
|
480
|
-
|
|
481
|
-
# The text as UTF-8 whatever the locale says: with no LANG/LC_* (an app
|
|
482
|
-
# started from Finder, launchd) stdin reads as US-ASCII and ARGV as
|
|
483
|
-
# binary. Invalid bytes become U+FFFD rather than an error.
|
|
484
|
-
def utf8(text)
|
|
485
|
-
text&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
486
|
-
end
|
|
487
465
|
end
|
|
488
466
|
end
|
|
@@ -277,6 +277,7 @@ module Samagotchi
|
|
|
277
277
|
("git #{rule.git}" if rule.git),
|
|
278
278
|
("touches #{rule.touches}" if rule.touches),
|
|
279
279
|
("rm #{rule.rm}" if rule.rm),
|
|
280
|
+
("memory #{rule.memory}" if rule.memory),
|
|
280
281
|
("models #{rule.models.join(",")}" if rule.models),
|
|
281
282
|
("modes #{rule.modes.join(",")}" if rule.modes),
|
|
282
283
|
("skip_read_only" if rule.skip_read_only)
|
|
@@ -39,7 +39,7 @@ module Samagotchi
|
|
|
39
39
|
# worker runs in its session's working directory)
|
|
40
40
|
def tool_activity_event(tool_name, call, result, registry: nil, cwd: Dir.pwd)
|
|
41
41
|
event = {
|
|
42
|
-
action: tool_activity_action(tool_name, registry: registry),
|
|
42
|
+
action: tool_activity_action(tool_name, registry: registry, call: call),
|
|
43
43
|
tool: tool_name,
|
|
44
44
|
params: tool_activity_params(tool_name, call, registry: registry),
|
|
45
45
|
status: tool_activity_status(result, tool_name)
|
|
@@ -151,14 +151,15 @@ module Samagotchi
|
|
|
151
151
|
parts.drop(base.length).join("/")
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
# @param call [Hash, nil] the call, for a label that depends on its form
|
|
155
|
+
def tool_activity_action(tool_name, registry: nil, call: nil)
|
|
155
156
|
case tool_name
|
|
156
157
|
when Tools::Execute::NAME then "running command"
|
|
157
158
|
when Tools::Read::NAME then "reading file"
|
|
158
159
|
when Tools::Write::NAME then "writing file"
|
|
159
160
|
when Tools::Edit::NAME then "editing file"
|
|
160
161
|
when Tools::MemoryRead::NAME then "reading memory"
|
|
161
|
-
when Tools::MemoryWrite::NAME then
|
|
162
|
+
when Tools::MemoryWrite::NAME then memory_write_action(call)
|
|
162
163
|
when Tools::TaskCreate::NAME then "starting background task"
|
|
163
164
|
when Tools::TaskGet::NAME then "checking task"
|
|
164
165
|
when Tools::TaskList::NAME then "listing tasks"
|
|
@@ -182,6 +183,15 @@ module Samagotchi
|
|
|
182
183
|
# whose task was stopped (the stop-task button, or the model's task_stop),
|
|
183
184
|
# and for a command the user's Stop killed or kept from starting: a
|
|
184
185
|
# cancel, not a failure (its text still starts "Error:" for the model).
|
|
186
|
+
# memory_write's label: a call with only a description changes the index line.
|
|
187
|
+
def memory_write_action(call)
|
|
188
|
+
return "saving memory" unless call.is_a?(Hash)
|
|
189
|
+
return "removing memory" if call[:remove].to_s.strip.downcase == "true"
|
|
190
|
+
return "updating memory description" if call[:content].to_s.empty? && !call[:description].to_s.strip.empty?
|
|
191
|
+
|
|
192
|
+
"saving memory"
|
|
193
|
+
end
|
|
194
|
+
|
|
185
195
|
def tool_activity_status(result, tool_name = nil)
|
|
186
196
|
text = result.to_s
|
|
187
197
|
return "stopped" if text == Tools::Execute::NOT_RUN_ON_STOP
|
|
@@ -232,6 +242,7 @@ module Samagotchi
|
|
|
232
242
|
parts << "scope=#{preview_tool_param(scope)}" unless scope.empty?
|
|
233
243
|
desc = call[:description].to_s.strip
|
|
234
244
|
parts << "description=#{preview_tool_param(desc)}" unless desc.empty?
|
|
245
|
+
parts << "remove=true" if call[:remove].to_s.strip.downcase == "true"
|
|
235
246
|
parts.join(" ")
|
|
236
247
|
when Tools::TaskCreate::NAME
|
|
237
248
|
parts = ["command=#{preview_tool_param(call[:content])}"]
|
|
@@ -127,7 +127,7 @@ module Samagotchi
|
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
name: "memory_write",
|
|
130
|
-
description: "Write or update a memory entry in scoped memories. Scope is required: project or system. Use the `name` parameter for the entry name (use `name`, NOT `path` — the file tools use `path`); each scope's index.md is auto-maintained (one managed line per entry); use name \"index\" to write the index file verbatim. For a small change to an existing memory (a step, a line), `edit` its file (<scope dir>/<name>.md) instead of rewriting it all; its index line is refreshed either way. If the user asks to save guidance for the current model only, pass current_model_only: true to save a model-specific overlay.",
|
|
130
|
+
description: "Write or update a memory entry in scoped memories. Scope is required: project or system. Use the `name` parameter for the entry name (use `name`, NOT `path` — the file tools use `path`); each scope's index.md is auto-maintained (one managed line per entry); use name \"index\" to write the index file verbatim. For a small change to an existing memory (a step, a line), `edit` its file (<scope dir>/<name>.md) instead of rewriting it all; its index line is refreshed either way. To change only an entry's index line, pass name, scope and description without content; to remove an entry, name, scope and remove: true. If the user asks to save guidance for the current model only, pass current_model_only: true to save a model-specific overlay.",
|
|
131
131
|
parameters: {
|
|
132
132
|
type: "object",
|
|
133
133
|
properties: {
|
|
@@ -137,7 +137,7 @@ module Samagotchi
|
|
|
137
137
|
},
|
|
138
138
|
content: {
|
|
139
139
|
type: "string",
|
|
140
|
-
description: "Markdown content to write"
|
|
140
|
+
description: "Markdown content to write; leave it out to change only the description"
|
|
141
141
|
},
|
|
142
142
|
scope: {
|
|
143
143
|
type: "string",
|
|
@@ -145,14 +145,18 @@ module Samagotchi
|
|
|
145
145
|
},
|
|
146
146
|
description: {
|
|
147
147
|
type: "string",
|
|
148
|
-
description: "Optional
|
|
148
|
+
description: "Optional one-line description (at most 200 characters) appended to the managed index line; without content, it changes only that line"
|
|
149
149
|
},
|
|
150
150
|
current_model_only: {
|
|
151
151
|
type: "boolean",
|
|
152
152
|
description: "Set true to save this entry as a model-specific overlay for the current model only (<name>.<model>.md); it is auto-appended when the entry is read under that model and never listed in the index."
|
|
153
|
+
},
|
|
154
|
+
remove: {
|
|
155
|
+
type: "boolean",
|
|
156
|
+
description: "Set true, with only name and scope, to remove the entry: its file (and model overlays) move to the bundle trash and its index line is dropped. Only when the user asked or a skill's steps say so."
|
|
153
157
|
}
|
|
154
158
|
},
|
|
155
|
-
required: %w[name
|
|
159
|
+
required: %w[name scope]
|
|
156
160
|
}
|
|
157
161
|
},
|
|
158
162
|
{
|
|
@@ -63,7 +63,7 @@ module Samagotchi
|
|
|
63
63
|
MemoryWrite::NAME => lambda do |call, kctx|
|
|
64
64
|
MemoryWrite.call(call[:content], path: call[:path], scope: call[:scope], description: call[:description],
|
|
65
65
|
current_model_only: truthy?(call[:current_model_only]),
|
|
66
|
-
model_key: kctx.model_key)
|
|
66
|
+
model_key: kctx.model_key, remove: truthy?(call[:remove]))
|
|
67
67
|
end,
|
|
68
68
|
Write::NAME => ->(call, _kctx) { Write.call(call[:content], path: call[:path]) },
|
|
69
69
|
Read::NAME => lambda do |call, _kctx|
|
|
@@ -150,13 +150,37 @@ module Samagotchi
|
|
|
150
150
|
/^- \*\*#{Regexp.escape(name)}\*\*[ \t]*(?:[·•].*|:.*)?(\r?\n|\z)/
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
# The longest description an index line takes: the line lands in
|
|
154
|
+
# every session's prompt for the repo.
|
|
155
|
+
DESCRIPTION_LIMIT = 200
|
|
156
|
+
|
|
157
|
+
# @param remove [Boolean] move the entry (and its model overlays) into
|
|
158
|
+
# the bundle trash and drop its index line (#remove)
|
|
159
|
+
def self.call(content, path:, scope:, description: nil, current_model_only: false, model_key: nil, remove: false)
|
|
154
160
|
entry_name = path.to_s.strip
|
|
155
161
|
body = content.to_s
|
|
156
162
|
return "Error: entry name is required" if entry_name.empty?
|
|
157
163
|
return MemoryRead.invalid_name_error(entry_name) if MemoryRead.invalid_name?(entry_name)
|
|
158
164
|
return "Error: scope is required" if scope.to_s.strip.empty?
|
|
159
|
-
|
|
165
|
+
|
|
166
|
+
if remove
|
|
167
|
+
if !body.empty? || !description.to_s.strip.empty? || current_model_only
|
|
168
|
+
return "Error: remove: true takes only name and scope (no content, description or current_model_only)"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
return remove(entry_name, scope)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
description = normalize_description(description)
|
|
175
|
+
if description && description.length > DESCRIPTION_LIMIT
|
|
176
|
+
return "Error: description is #{description.length} characters; the limit is #{DESCRIPTION_LIMIT} " \
|
|
177
|
+
"(one line: what the memory is for)"
|
|
178
|
+
end
|
|
179
|
+
if body.empty?
|
|
180
|
+
return "Error: content is required (or pass only description to change the index line)" unless description
|
|
181
|
+
|
|
182
|
+
return update_description(entry_name, scope, description, current_model_only)
|
|
183
|
+
end
|
|
160
184
|
|
|
161
185
|
resolved_scope = MemoryRead.normalize_scope(scope)
|
|
162
186
|
|
|
@@ -205,6 +229,83 @@ module Samagotchi
|
|
|
205
229
|
"Error: #{e.message}"
|
|
206
230
|
end
|
|
207
231
|
|
|
232
|
+
# A description as the index line takes it: whitespace (newlines too)
|
|
233
|
+
# collapsed to single spaces; nil when blank, which keeps the stored one.
|
|
234
|
+
def self.normalize_description(description)
|
|
235
|
+
text = description.to_s.gsub(/\s+/, " ").strip
|
|
236
|
+
text.empty? ? nil : text
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# The description-only form: name, scope and description, no content.
|
|
240
|
+
# Rewrites that entry's index line (size and date too) and leaves its
|
|
241
|
+
# file alone, so a status kept in the description costs no rewrite of
|
|
242
|
+
# the body.
|
|
243
|
+
def self.update_description(entry_name, scope, description, current_model_only)
|
|
244
|
+
return "Error: the index has no index line of its own; pass content to write it" if entry_name == MEMORY_INDEX
|
|
245
|
+
if current_model_only
|
|
246
|
+
return "Error: a model overlay has no index line; pass content to write the overlay, or drop " \
|
|
247
|
+
"current_model_only to change the base entry's description"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
resolved_scope = MemoryRead.normalize_scope(scope)
|
|
251
|
+
file_path = File.join(MemoryRead.memories_dir(resolved_scope), "#{entry_name}.md")
|
|
252
|
+
return "Error: no memory '#{entry_name}' in #{resolved_scope} scope; pass content to create it" unless File.file?(file_path)
|
|
253
|
+
|
|
254
|
+
manage_index(resolved_scope, entry_name, File.size(file_path), description)
|
|
255
|
+
"Memory '#{entry_name}' description updated in #{resolved_scope} scope (file unchanged)."
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# remove: true. Moves <name>.md and its model overlays into one
|
|
259
|
+
# MemoryBundle::Trash dir (memory-<name>-<time>, which `chi bundle
|
|
260
|
+
# trash` lists and empties) and drops the index line; a line whose
|
|
261
|
+
# file is already gone is dropped alone. A memory a bundle owns (an
|
|
262
|
+
# installed bundle's record lists it, or its line says "· from
|
|
263
|
+
# <bundle>") is the uninstaller's, and the index isn't a memory.
|
|
264
|
+
def self.remove(entry_name, scope)
|
|
265
|
+
return "Error: the index can't be removed; it is kept up to date for you" if entry_name == MEMORY_INDEX
|
|
266
|
+
|
|
267
|
+
require_relative "../memory_bundle/index_updater"
|
|
268
|
+
require_relative "../memory_bundle/provenance"
|
|
269
|
+
require_relative "../memory_bundle/trash"
|
|
270
|
+
resolved_scope = MemoryRead.normalize_scope(scope)
|
|
271
|
+
dir = MemoryRead.memories_dir(resolved_scope)
|
|
272
|
+
index = File.join(dir, "#{MEMORY_INDEX}.md")
|
|
273
|
+
index_text = File.file?(index) ? File.read(index, encoding: "UTF-8") : ""
|
|
274
|
+
line = index_text[managed_pattern(entry_name)]
|
|
275
|
+
file = File.join(dir, "#{entry_name}.md")
|
|
276
|
+
|
|
277
|
+
owner = Samagotchi::MemoryBundle::Provenance.claimants("#{entry_name}.md", scope: resolved_scope).first ||
|
|
278
|
+
(line && Samagotchi::MemoryBundle::IndexUpdater.extract_source(line))
|
|
279
|
+
return "Error: memory '#{entry_name}' came from bundle #{owner}; `chi bundle uninstall #{owner}` removes it" if owner
|
|
280
|
+
|
|
281
|
+
unless File.file?(file)
|
|
282
|
+
return "Error: no memory '#{entry_name}' in #{resolved_scope} scope" unless line
|
|
283
|
+
|
|
284
|
+
Samagotchi::MemoryBundle::IndexUpdater.remove_index(resolved_scope, entry_name)
|
|
285
|
+
return "Memory '#{entry_name}' had no file in #{resolved_scope} scope; its dangling index line was dropped."
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
trash = Samagotchi::MemoryBundle::Trash.new("memory-#{entry_name}")
|
|
289
|
+
([file] + overlay_files(dir, entry_name, index_text)).each { |path| trash.move(path) }
|
|
290
|
+
Samagotchi::MemoryBundle::IndexUpdater.remove_index(resolved_scope, entry_name)
|
|
291
|
+
"Memory '#{entry_name}' removed from #{resolved_scope} scope: moved to #{trash.dir} " \
|
|
292
|
+
"(`chi bundle trash` lists and empties it); index line dropped."
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# The entry's model overlays: <name>.<key>.md with a key ModelOverlay
|
|
296
|
+
# makes, unless that stem is a memory of its own (names may have dots:
|
|
297
|
+
# handoff_x.v2 has an index line).
|
|
298
|
+
def self.overlay_files(dir, entry_name, index_text)
|
|
299
|
+
pattern = /\A#{Regexp.escape(entry_name)}\.([a-z0-9-]+)\.md\z/
|
|
300
|
+
Dir.children(dir).sort.filter_map do |child|
|
|
301
|
+
key = child[pattern, 1] or next
|
|
302
|
+
next if index_text.match?(managed_pattern("#{entry_name}.#{key}"))
|
|
303
|
+
|
|
304
|
+
path = File.join(dir, child)
|
|
305
|
+
path if File.file?(path)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
208
309
|
# Adds or refreshes a single managed line for `entry_name` in the given
|
|
209
310
|
# scope's index.md, preserving every other byte byte-for-byte.
|
|
210
311
|
# Delegates to IndexUpdater to maintain a single source of truth.
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -61,8 +61,9 @@ export function approvalView(pq) {
|
|
|
61
61
|
const who = a.rule ? [`rule ${a.rule}`, a.source].filter(Boolean).join(", ") : (a.source || "hook");
|
|
62
62
|
const reason = a.reason ? String(a.reason) : "(no reason given)";
|
|
63
63
|
return {
|
|
64
|
-
// A plugin tool's label ("chrome: screenshot"), as its row shows it
|
|
65
|
-
tool
|
|
64
|
+
// A plugin tool's label ("chrome: screenshot"), as its row shows it;
|
|
65
|
+
// else the tool the call acts as (an MCP tool behind mcp_call).
|
|
66
|
+
tool: a.label || a.acts_as || a.tool || "",
|
|
66
67
|
// A tool without a command or path (an MCP tool): its compact args.
|
|
67
68
|
what: command || paths.join("\n") || a.args || "",
|
|
68
69
|
isCommand: !!command,
|
|
@@ -142,7 +143,7 @@ export function summaryText(pq, { answer = null, cancelled = false, reason = ""
|
|
|
142
143
|
const text = asked ? `${truncate(asked, 60)} → ${result}` : result;
|
|
143
144
|
return truncate(relay ? `${relay.who}: ${text}` : text, 120);
|
|
144
145
|
}
|
|
145
|
-
const label = isApproval(pq) ? `Approve ${pq.approval?.label || pq.approval?.tool || "call"}?`
|
|
146
|
+
const label = isApproval(pq) ? `Approve ${pq.approval?.label || pq.approval?.acts_as || pq.approval?.tool || "call"}?`
|
|
146
147
|
: isContinue(pq) ? "Step limit" : "Question";
|
|
147
148
|
return truncate(pq.header || label);
|
|
148
149
|
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.37.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -167,6 +167,9 @@ files:
|
|
|
167
167
|
- lib/samagotchi/broadcast/recipients.rb
|
|
168
168
|
- lib/samagotchi/broadcast/scope_card.rb
|
|
169
169
|
- lib/samagotchi/broadcast/tags.rb
|
|
170
|
+
- lib/samagotchi/broadcast/triage.rb
|
|
171
|
+
- lib/samagotchi/broadcast/triage_log.rb
|
|
172
|
+
- lib/samagotchi/broadcast/triage_model.rb
|
|
170
173
|
- lib/samagotchi/broadcast_command.rb
|
|
171
174
|
- lib/samagotchi/bundle_command.rb
|
|
172
175
|
- lib/samagotchi/bundle_needs.rb
|
|
@@ -284,6 +287,7 @@ files:
|
|
|
284
287
|
- lib/samagotchi/idle_recap.rb
|
|
285
288
|
- lib/samagotchi/idle_reminders.rb
|
|
286
289
|
- lib/samagotchi/idle_scheduler.rb
|
|
290
|
+
- lib/samagotchi/idle_target.rb
|
|
287
291
|
- lib/samagotchi/image_store.rb
|
|
288
292
|
- lib/samagotchi/installed_gem.rb
|
|
289
293
|
- lib/samagotchi/installed_versions.rb
|