samagotchi 0.35.0 → 0.36.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.
@@ -33,14 +33,16 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
33
33
  - Parameters: `name: "entry_name"`, `content: "..."`, `scope: "project"|"system"`, optional `description: "one-liner"`, optional `current_model_only: true`.
34
34
  - `name` is the entry name **without** `.md` (the tool adds it). Use `name`, **not** `path` — `path` belongs to the file tools and is ignored here. `name: "index"` is a verbatim write to `index.md` (no auto-index update) — rarely needed.
35
35
  - `scope` is **required** — never omit. Prefer `project` for repo conventions, `system` for user preferences.
36
- - `description` is appended to the managed `index.md` line (`— description`). Replaces previous description if given; otherwise preserves existing one.
36
+ - `description` is appended to the managed `index.md` line (`— description`): one line, at most 200 characters (longer is refused; newlines become spaces). Replaces previous description if given; a missing or blank one preserves the existing one.
37
+ - To change only the description, pass `name`, `scope` and `description` without `content`: the index line changes (its date becomes today), the file stays as it is. The memory must exist in that scope.
38
+ - To remove a memory, pass `name`, `scope` and `remove: true` (nothing else): its file and model overlays move to the bundle trash (`chi bundle trash` lists and empties it) and its index line goes, a dangling line too. Only when the user asked or a skill's steps say so; the guardrails ask the user first. A memory a bundle installed is refused: `chi bundle uninstall` owns it.
37
39
  - For a small change to an existing memory (a step, a line), `edit` its file instead of rewriting it all: the path is in `memory_write`'s result and the scope dirs are in the prompt. Its index line is refreshed either way.
38
40
  - On success returns `Memory 'name' saved to <scope> scope (N bytes). File written: <path> Index line refreshed automatically.` — confirm `bytes` and `scope`; the index needs nothing from you.
39
41
 
40
42
  **When to write:**
41
43
  - After learning a durable preference (e.g. commit style, test command, coding guideline) that the user confirmed or you observed repeatedly — ask before overwriting existing entries where appropriate.
42
44
  - Keep entries small and focused (one topic per file). Use clear filenames: `commit_preferences`, `testing_guide`, `project_conventions`.
43
- - Never store secrets, tokens, or transient state. Memories are shared via bundles.
45
+ - Never store secrets, tokens, or transient state. Memories are shared via bundles. One exception: a skill may keep a working handoff memory (its steps say so) whose status is its description, removed when the work is done.
44
46
 
45
47
  **Placeholders:**
46
48
  - Content may contain placeholder hints written as double-curly braces around a name (e.g., test_command, language). Detected by `Placeholder` (`Placeholder::PLACEHOLDER_RE`) — install warns but does not fail. Fill them when you write. The placeholder syntax is two opening braces, a name, two closing braces.
@@ -46,6 +46,9 @@ module Samagotchi
46
46
  # one). samagotchi/context stops before a word character or a dot, so
47
47
  # chi's own lib/samagotchi/context_*.rb isn't one.
48
48
  CHI_TEXT = %r{\.config/samagotchi|samagotchi/config\.yml|samagotchi/hooks|samagotchi/guardrails|samagotchi/context(?![\w.])|memories/\.bundles}
49
+ # The guardrails bundle's ask before a memory removal: its paths are in
50
+ # the config dir, yet a parent may answer it.
51
+ MEMORY_REMOVE_RULE = "memory-remove"
49
52
  # The guardrails bundle's shell-touches-chi, for a word it can't
50
53
  # resolve to a path (Guardrails::ShellPaths): CHI_TEXT and .git/hooks.
51
54
  CHI_SHELL_TEXT = Regexp.union(CHI_TEXT, %r{\.git/hooks})
@@ -94,6 +97,9 @@ module Samagotchi
94
97
  return false unless facts.is_a?(Hash)
95
98
  return true if PROTECTED_RULES.include?(fetch(facts, :rule).to_s)
96
99
  return true if fetch(facts, :source).to_s == ProtectedPaths::SOURCE
100
+ # A memory's file sits in the config dir, but removing one is the
101
+ # memory-remove rule's ask, which a parent may answer.
102
+ return false if fetch(facts, :rule).to_s == MEMORY_REMOVE_RULE
97
103
 
98
104
  dirs = chi_dirs
99
105
  paths = Array(fetch(facts, :paths)).map(&:to_s).reject(&:empty?)
@@ -28,6 +28,7 @@ module Samagotchi
28
28
  # modes: "strict", "auto" or a list of them: the rule only votes
29
29
  # in those guardrails.mode values (missing = every mode;
30
30
  # a rule from config.yml without it votes in every mode)
31
+ # memory: "remove": a memory_write call with remove: true
31
32
  # skip_read_only: true: a shell command that only reads
32
33
  # (ReadOnlyShell) doesn't match
33
34
  # verdict ask|deny, reason, scopes (for an ask; default all).
@@ -38,14 +39,14 @@ module Samagotchi
38
39
  class Rules
39
40
  class ParseError < StandardError; end
40
41
 
41
- KEYS = %w[id tool command path git touches rm models modes skip_read_only verdict reason scopes].freeze
42
- MATCH_KEYS = %w[tool command path git touches rm].freeze
42
+ KEYS = %w[id tool command path git touches rm memory models modes skip_read_only verdict reason scopes].freeze
43
+ MATCH_KEYS = %w[tool command path git touches rm memory].freeze
43
44
  VERDICTS = %w[ask deny].freeze
44
45
  # guardrails.mode's values.
45
46
  MODES = %w[auto strict].freeze
46
47
  GLOB_FLAGS = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
47
48
 
48
- Rule = Struct.new(:id, :tools, :command, :path, :git, :touches, :rm, :models, :modes, :skip_read_only, :verdict, :reason,
49
+ Rule = Struct.new(:id, :tools, :command, :path, :git, :touches, :rm, :memory, :models, :modes, :skip_read_only, :verdict, :reason,
49
50
  :scopes, :source, keyword_init: true) do
50
51
  def matches?(targets)
51
52
  return false unless targets
@@ -56,6 +57,7 @@ module Samagotchi
56
57
  return false if git && !targets.git_outside_repo?
57
58
  return false if touches && !targets.touches_chi?
58
59
  return false if rm && !targets.rm_outside_tmp?
60
+ return false if memory && !targets.memory_remove?
59
61
  return false if skip_read_only && targets.read_only?
60
62
 
61
63
  true
@@ -128,7 +130,8 @@ module Samagotchi
128
130
 
129
131
  Rule.new(id: id, tools: tools_of(raw["tool"], label), command: regex_of(raw["command"], label),
130
132
  path: path_of(raw["path"], label), git: git_of(raw["git"], label),
131
- touches: touches_of(raw["touches"], label), rm: rm_of(raw["rm"], label), models: models_of(raw["models"], label),
133
+ touches: touches_of(raw["touches"], label), rm: rm_of(raw["rm"], label), memory: memory_of(raw["memory"], label),
134
+ models: models_of(raw["models"], label),
132
135
  modes: modes_of(raw["modes"], label),
133
136
  skip_read_only: flag_of(raw, "skip_read_only", label), verdict: verdict.to_sym,
134
137
  reason: (raw["reason"] || "rule #{id}").to_s, scopes: scopes_of(raw["scopes"], label), source: source)
@@ -185,6 +188,14 @@ module Samagotchi
185
188
  value
186
189
  end
187
190
 
191
+ # Only "remove" for now.
192
+ def self.memory_of(value, label)
193
+ return nil if value.nil?
194
+ raise ParseError, "#{label}: memory must be remove" unless value == "remove"
195
+
196
+ value
197
+ end
198
+
188
199
  def self.modes_of(value, label)
189
200
  return nil if value.nil?
190
201
 
@@ -42,6 +42,11 @@ module Samagotchi
42
42
  # chi's own tools
43
43
  attr_reader :args
44
44
 
45
+ # @return [Boolean] a memory_write call that removes a memory (remove:
46
+ # true), for a rule's memory: remove
47
+ attr_reader :memory_remove
48
+ alias memory_remove? memory_remove
49
+
45
50
  # @param call [Hash] the parsed tool call
46
51
  # @param context [Context]
47
52
  # @param model_key [String, nil] for a memory_write model overlay, and
@@ -76,8 +81,9 @@ module Samagotchi
76
81
  entry = registry && registry[tool]
77
82
  args = call[:args] if entry && !entry.core? && call[:args].is_a?(Hash)
78
83
  end
84
+ memory_remove = tool == "memory_write" && call[:remove].to_s.strip.downcase == "true"
79
85
  new(tool: tool, command: command, paths: paths.compact, cwd: cwd, repo_root: context.repo_root(cwd),
80
- repo: context.repo(cwd), args: args,
86
+ repo: context.repo(cwd), args: args, memory_remove: memory_remove,
81
87
  model_name: model_name, model_key: model_key, session_root: context.repo_root(base) || base,
82
88
  chi_dirs: -> { chi_dirs(context, base) })
83
89
  end
@@ -128,7 +134,8 @@ module Samagotchi
128
134
 
129
135
  # @param chi_dirs [#call, nil] → the dirs touches_chi? looks for
130
136
  def initialize(tool:, command:, paths:, cwd:, repo_root:, repo: nil, args: nil, model_name: nil, model_key: nil,
131
- session_root: nil, chi_dirs: nil)
137
+ session_root: nil, chi_dirs: nil, memory_remove: false)
138
+ @memory_remove = memory_remove
132
139
  @repo = repo
133
140
  @chi_dirs = chi_dirs
134
141
  @session_root = session_root || repo_root || cwd
@@ -25,7 +25,7 @@ module Samagotchi
25
25
  pattern = managed_pattern(entry_name)
26
26
  if content.match?(pattern)
27
27
  existing = content[pattern].to_s.chomp
28
- resolved_desc = description&.to_s&.strip || extract_description(existing)
28
+ resolved_desc = blank_description?(description) ? extract_description(existing) : description
29
29
  resolved_source = source.nil? ? extract_source(existing) : (source || nil)
30
30
  new_line = managed_line(entry_name, scope, byte_count, resolved_desc, resolved_source)
31
31
  next content.sub(pattern) { new_line + $1 }
@@ -72,11 +72,14 @@ module Samagotchi
72
72
  def self.managed_line(name, scope, byte_count, description, source = nil)
73
73
  line = "- **#{name}** · #{scope} · #{date_str} · #{byte_count}"
74
74
  line += " · from #{source}" unless source.to_s.strip.empty?
75
- desc = description&.to_s&.strip
76
- line += " — #{desc}" unless desc.to_s.empty?
75
+ desc = description.to_s.gsub(/\s+/, " ").strip
76
+ line += " — #{desc}" unless desc.empty?
77
77
  line
78
78
  end
79
79
 
80
+ # A blank description (nil, "", whitespace) keeps the line's own.
81
+ def self.blank_description?(description) = description.to_s.strip.empty?
82
+
80
83
  def self.extract_description(line)
81
84
  if line =~ /\s—\s(.*)\s*\z/
82
85
  $1
@@ -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
- def tool_activity_action(tool_name, registry: nil)
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 "saving memory"
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 short description appended to the managed index line"
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 content scope]
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
- def self.call(content, path:, scope:, description: nil, current_model_only: false, model_key: nil)
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
- return "Error: content is required" if body.empty?
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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.35.0"
4
+ VERSION = "0.36.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
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.35.0
4
+ version: 0.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov