samagotchi 0.2.0 → 0.4.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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +198 -1
  3. data/README.md +56 -4
  4. data/bin/chi +118 -50
  5. data/docs/cli.md +184 -9
  6. data/docs/configuration.md +333 -47
  7. data/docs/desktop.md +45 -4
  8. data/docs/guardrails.md +11 -0
  9. data/docs/hooks.md +208 -5
  10. data/docs/plugins.md +68 -2
  11. data/docs/releasing.md +23 -13
  12. data/docs/sessions.md +45 -17
  13. data/lib/samagotchi/answer_display.rb +95 -0
  14. data/lib/samagotchi/archive_store.rb +90 -0
  15. data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
  16. data/lib/samagotchi/bootstrap/probe.rb +262 -0
  17. data/lib/samagotchi/bootstrap_command.rb +347 -0
  18. data/lib/samagotchi/bridge/pending_card.rb +89 -0
  19. data/lib/samagotchi/bridge/turn_accumulator.rb +15 -3
  20. data/lib/samagotchi/bridge.rb +13 -1
  21. data/lib/samagotchi/bridge_client.rb +6 -2
  22. data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
  23. data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
  24. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +531 -0
  25. data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
  26. data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
  27. data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
  28. data/lib/samagotchi/bundles/system/delegated.md +6 -7
  29. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  30. data/lib/samagotchi/bundles/system/self_map.md +8 -2
  31. data/lib/samagotchi/client.rb +81 -19
  32. data/lib/samagotchi/commands/registry.rb +8 -0
  33. data/lib/samagotchi/config.rb +252 -48
  34. data/lib/samagotchi/desktop/macos/App.swift +12 -8
  35. data/lib/samagotchi/desktop/macos/ChiRunner.swift +17 -9
  36. data/lib/samagotchi/desktop/macos/Images.swift +113 -0
  37. data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
  38. data/lib/samagotchi/desktop/macos/Panel.swift +180 -25
  39. data/lib/samagotchi/desktop/macos.rb +59 -8
  40. data/lib/samagotchi/desktop_command.rb +6 -3
  41. data/lib/samagotchi/edit_preview.rb +82 -0
  42. data/lib/samagotchi/empty_answer_retry.rb +43 -0
  43. data/lib/samagotchi/engine.rb +434 -140
  44. data/lib/samagotchi/gem_update.rb +89 -0
  45. data/lib/samagotchi/guardrails/approval.rb +35 -4
  46. data/lib/samagotchi/guardrails/load_failures.rb +9 -3
  47. data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
  48. data/lib/samagotchi/guardrails.rb +1 -0
  49. data/lib/samagotchi/hooks/registry.rb +24 -5
  50. data/lib/samagotchi/host_registry.rb +9 -12
  51. data/lib/samagotchi/idle_client.rb +24 -15
  52. data/lib/samagotchi/idle_recap.rb +5 -1
  53. data/lib/samagotchi/idle_reminders.rb +2 -2
  54. data/lib/samagotchi/image_store.rb +10 -6
  55. data/lib/samagotchi/kernel_loop.rb +73 -94
  56. data/lib/samagotchi/live_versions.rb +59 -0
  57. data/lib/samagotchi/llm/api_key.rb +41 -0
  58. data/lib/samagotchi/llm/chat_loop.rb +132 -29
  59. data/lib/samagotchi/llm/errors.rb +41 -9
  60. data/lib/samagotchi/llm/http.rb +57 -17
  61. data/lib/samagotchi/llm/openai_chat.rb +17 -30
  62. data/lib/samagotchi/log_subscriber.rb +18 -3
  63. data/lib/samagotchi/memory_bundle/installer.rb +65 -63
  64. data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
  65. data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
  66. data/lib/samagotchi/memory_bundle/status.rb +4 -1
  67. data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
  68. data/lib/samagotchi/model_profile.rb +24 -1
  69. data/lib/samagotchi/plugin/context.rb +22 -1
  70. data/lib/samagotchi/plugin/sessions.rb +3 -1
  71. data/lib/samagotchi/prompt.rb +4 -2
  72. data/lib/samagotchi/reminder_store.rb +1 -9
  73. data/lib/samagotchi/reply_wait.rb +126 -0
  74. data/lib/samagotchi/sampling_settings.rb +58 -0
  75. data/lib/samagotchi/self_report.rb +18 -3
  76. data/lib/samagotchi/send_command.rb +252 -11
  77. data/lib/samagotchi/session.rb +52 -11
  78. data/lib/samagotchi/session_archive_command.rb +107 -0
  79. data/lib/samagotchi/session_commands.rb +46 -7
  80. data/lib/samagotchi/session_manager.rb +115 -25
  81. data/lib/samagotchi/session_metrics.rb +222 -106
  82. data/lib/samagotchi/steer.rb +72 -0
  83. data/lib/samagotchi/terminal_ui/attached_loop.rb +57 -28
  84. data/lib/samagotchi/terminal_ui/event_renderer.rb +21 -11
  85. data/lib/samagotchi/terminal_ui/formatting.rb +40 -8
  86. data/lib/samagotchi/terminal_ui/input_support.rb +7 -19
  87. data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
  88. data/lib/samagotchi/terminal_ui.rb +134 -247
  89. data/lib/samagotchi/text_diff.rb +181 -0
  90. data/lib/samagotchi/thinking.rb +115 -0
  91. data/lib/samagotchi/tool_activity.rb +3 -1
  92. data/lib/samagotchi/tool_runner.rb +34 -1
  93. data/lib/samagotchi/tools/ask_user_question.rb +41 -33
  94. data/lib/samagotchi/tools/builtins.rb +15 -4
  95. data/lib/samagotchi/tools/delegate_wait.rb +26 -69
  96. data/lib/samagotchi/tools/edit.rb +23 -9
  97. data/lib/samagotchi/tools/execute.rb +52 -14
  98. data/lib/samagotchi/tools/task_runtime.rb +19 -0
  99. data/lib/samagotchi/tools/task_wait.rb +27 -3
  100. data/lib/samagotchi/tools/write.rb +4 -0
  101. data/lib/samagotchi/turn_flow.rb +12 -2
  102. data/lib/samagotchi/turn_note.rb +60 -6
  103. data/lib/samagotchi/update_command.rb +308 -0
  104. data/lib/samagotchi/update_hint.rb +59 -0
  105. data/lib/samagotchi/version.rb +1 -1
  106. data/lib/samagotchi/vision_support.rb +7 -9
  107. data/lib/samagotchi/web/app.rb +91 -7
  108. data/lib/samagotchi/web/message_parts.rb +8 -3
  109. data/lib/samagotchi/web/public/activity.js +13 -1
  110. data/lib/samagotchi/web/public/annotate_presets.js +26 -0
  111. data/lib/samagotchi/web/public/annotations.js +13 -0
  112. data/lib/samagotchi/web/public/app.js +472 -111
  113. data/lib/samagotchi/web/public/card.js +5 -3
  114. data/lib/samagotchi/web/public/chat_view.js +13 -1
  115. data/lib/samagotchi/web/public/copy.js +20 -4
  116. data/lib/samagotchi/web/public/ctx.js +15 -0
  117. data/lib/samagotchi/web/public/data.js +23 -6
  118. data/lib/samagotchi/web/public/diff_view.js +58 -0
  119. data/lib/samagotchi/web/public/format.js +9 -0
  120. data/lib/samagotchi/web/public/index.html +60 -3
  121. data/lib/samagotchi/web/public/notify.js +175 -0
  122. data/lib/samagotchi/web/public/question_card.js +5 -2
  123. data/lib/samagotchi/web/public/sessions_list.js +7 -0
  124. data/lib/samagotchi/web/public/timing.js +39 -14
  125. data/lib/samagotchi/web/public/turn_events.js +75 -5
  126. data/lib/samagotchi/web/public/turn_view.js +49 -8
  127. data/lib/samagotchi/web/server.rb +8 -4
  128. data/lib/samagotchi/web/session_hub.rb +2 -1
  129. data/lib/samagotchi/web/session_summary.rb +24 -1
  130. data/lib/samagotchi/worker.rb +16 -4
  131. metadata +31 -1
data/bin/chi CHANGED
@@ -38,16 +38,18 @@ if ARGV[0] == "sessions"
38
38
  sessions_args = ARGV[1..] || []
39
39
  sub = sessions_args[0]
40
40
  if sub.nil? || %w[-h --help help].include?(sub)
41
- puts "Usage: chi sessions <list|stop|delete|prune|clean> [options]"
41
+ puts "Usage: chi sessions <list|stop|archive|unarchive|delete|prune|clean> [options]"
42
42
  puts " list [--sort updated_at|created_at] [--order desc|asc] [--limit N]"
43
- puts " [--live] [--cwd PATH] [--format text|json|tsv]"
43
+ puts " [--live] [--cwd PATH] [--format text|json|tsv] [--archived]"
44
44
  puts " --live: sessions a worker runs now (the ones chi note reaches), 10 unless --limit"
45
45
  puts " --cwd PATH: sessions in PATH or below; json/tsv (id<TAB>description) are for scripts"
46
46
  puts " [--scope=all]: every project's sessions; by default only this git project's (all outside a repo)"
47
+ puts " --archived: archived sessions too (marked [archived]; json: archived: true)"
47
48
  puts " stop ID... # stop each session's worker (IDs or unique prefixes); chi --resume ID then starts a fresh one"
49
+ puts " archive ID... # hide sessions (and their delegates) from every list and keep them for good; unarchive ID... brings them back"
48
50
  puts " delete [--force] ID... # delete sessions for good (IDs or unique prefixes); --force stops a live worker first"
49
51
  puts " prune [--dry-run] [--days N] [--keep N] [--keep-status running,...] [--test-only]"
50
- puts " clean [--dry-run] [--days N] # test sessions (SAMAGOTCHI_ENV=test, CI): all of them, or those older than N days"
52
+ puts " clean [--dry-run] [--days N] # test sessions (SAMAGOTCHI_ENV=test, CI) and leftover chi scratch ones: all of them, or those older than N days"
51
53
  puts "Defaults: days=14 keep=500 keep_status=running (env overrides: SAMAGOTCHI_SESSION_RETENTION_DAYS etc.)"
52
54
  exit 0
53
55
  end
@@ -60,6 +62,7 @@ if ARGV[0] == "sessions"
60
62
  order = nil
61
63
  limit = nil
62
64
  live = sessions_args.include?("--live")
65
+ include_archived = sessions_args.include?("--archived")
63
66
  cwd = nil
64
67
  format = nil
65
68
  scope = nil
@@ -89,6 +92,9 @@ if ARGV[0] == "sessions"
89
92
  when "delete"
90
93
  require "samagotchi/session_delete_command"
91
94
  exit Samagotchi::SessionDeleteCommand.new(sessions_args[1..]).run
95
+ when "archive", "unarchive"
96
+ require "samagotchi/session_archive_command"
97
+ exit Samagotchi::SessionArchiveCommand.new(sub, sessions_args[1..]).run
92
98
  when "list"
93
99
  unless format.nil? || %w[text json tsv].include?(format)
94
100
  warn "Unknown format #{format.inspect}: use --format text|json|tsv"
@@ -107,7 +113,8 @@ if ARGV[0] == "sessions"
107
113
  # and test runs stay out.
108
114
  if live || cwd || (format && format != "text")
109
115
  summaries = Samagotchi::SessionManager.session_summaries(
110
- live: live, cwd: cwd, limit: limit || (live ? 10 : nil), include_tests: false, project_root: project
116
+ live: live, cwd: cwd, limit: limit || (live ? 10 : nil), include_tests: false, project_root: project,
117
+ include_archived: include_archived
111
118
  )
112
119
  case format
113
120
  when "json"
@@ -115,7 +122,11 @@ if ARGV[0] == "sessions"
115
122
  # or nil; recap: the saved recap's first sentence, or nil; project:
116
123
  # its git project's root, or nil
117
124
  # parent_id: the session that delegated it (the delegate tool), or nil
118
- keys = %i[id short_id desc cwd project updated_at live busy owner recap parent_id]
125
+ # archived: hidden from the lists (only with --archived can it be true)
126
+ # scratch: a `chi scratch` session (deleted when its REPL ends, a
127
+ # leftover one at the next sweep)
128
+ # ctx_pct: how full the context was after the last turn, or nil
129
+ keys = %i[id short_id desc cwd project updated_at live busy owner recap parent_id archived scratch ctx_pct]
119
130
  puts JSON.generate(summaries.map { |summary| summary.slice(*keys) })
120
131
  when "tsv"
121
132
  summaries.each { |summary| puts "#{summary[:id]}\t#{summary[:desc]}" }
@@ -131,7 +142,10 @@ if ARGV[0] == "sessions"
131
142
  end
132
143
  # A delegated session points at its parent.
133
144
  child = summary[:parent_short_id] ? " ↳ #{summary[:parent_short_id]}" : ""
134
- puts "#{summary[:id]} #{state.ljust(8)} #{summary[:updated_at]} #{desc}#{child}"
145
+ flag = summary[:scratch] ? " [scratch]" : ""
146
+ flag += " [archived]" if summary[:archived]
147
+ ctx = Samagotchi::SessionMetrics.context_label(summary[:ctx_pct])
148
+ puts "#{summary[:id]} #{state.ljust(8)} #{ctx.ljust(8)} #{summary[:updated_at]} #{desc}#{flag}#{child}"
135
149
  end
136
150
  if project
137
151
  puts "#{summaries.empty? ? "" : "\n"}#{scope_note.call(summaries.size)}"
@@ -143,7 +157,8 @@ if ARGV[0] == "sessions"
143
157
  end
144
158
  sort ||= "updated_at"
145
159
  order ||= "desc"
146
- sessions = Samagotchi::SessionManager.list_sessions(sort: sort, order: order, limit: limit, project_root: project)
160
+ sessions = Samagotchi::SessionManager.list_sessions(sort: sort, order: order, limit: limit, project_root: project,
161
+ include_archived: include_archived)
147
162
  # The saved recap's first sentence, else the last prompt, cut as before.
148
163
  state_dir = Samagotchi::Session.default_state_dir
149
164
  list_text = lambda do |s|
@@ -152,9 +167,14 @@ if ARGV[0] == "sessions"
152
167
  end
153
168
  # A delegated session points at its parent: ↳ <parent's short id>.
154
169
  row = lambda do |s|
155
- flag = s.test_run ? " [test]" : ""
170
+ flag = s.scratch ? " [scratch]" : (s.test_run ? " [test]" : "")
171
+ flag += " [archived]" if s.archived
156
172
  child = s.parent_id ? " ↳ #{s.parent_id[0, 8]}" : ""
157
- "#{s.id} #{s.status.ljust(8)} #{s.updated_at} #{list_text.call(s)}#{flag}#{child}"
173
+ # How full the context was after the last turn: "ctx 12%", blank when unknown.
174
+ ctx = Samagotchi::SessionMetrics.context_label(
175
+ Samagotchi::SessionMetrics.saved_context_pct(Samagotchi::Session.session_dir(s.id, state_dir: state_dir))
176
+ )
177
+ "#{s.id} #{s.status.ljust(8)} #{ctx.ljust(8)} #{s.updated_at} #{list_text.call(s)}#{flag}#{child}"
158
178
  end
159
179
  if project
160
180
  sessions.each { |s| puts row.call(s) }
@@ -214,7 +234,7 @@ if ARGV[0] == "sessions"
214
234
  end
215
235
  exit 0
216
236
  else
217
- warn "Unknown sessions subcommand: #{sub}. Use: list, stop, delete, prune, clean"
237
+ warn "Unknown sessions subcommand: #{sub}. Use: list, stop, archive, unarchive, delete, prune, clean"
218
238
  exit 1
219
239
  end
220
240
  end
@@ -231,6 +251,18 @@ if ARGV[0] == "send"
231
251
  exit Samagotchi::SendCommand.new(ARGV[1..] || []).run
232
252
  end
233
253
 
254
+ # `chi bootstrap`: find a model server and write config.yml, before OptionParser (--model, --name are its own)
255
+ if ARGV[0] == "bootstrap"
256
+ require "samagotchi/bootstrap_command"
257
+ exit Samagotchi::BootstrapCommand.new(ARGV[1..] || []).run
258
+ end
259
+
260
+ # `chi update`: bring an installed chi up to date, before OptionParser (its own flags)
261
+ if ARGV[0] == "update"
262
+ require "samagotchi/update_command"
263
+ exit Samagotchi::UpdateCommand.new(ARGV[1..] || []).run
264
+ end
265
+
234
266
  # `chi desktop`: the native "Send to chi" helper, before OptionParser (its own flags)
235
267
  if ARGV[0] == "desktop"
236
268
  require "samagotchi/desktop_command"
@@ -346,7 +378,7 @@ if ARGV[0] == "bundle"
346
378
  lines << ""
347
379
  lines << "Your task: resolve each conflict by editing the file in place using the memory_write tool."
348
380
  lines << "Use memory_read to inspect current content if needed. The correct scope for memory_write is the bundle's scope."
349
- lines << "When all conflicts are resolved, type /exit to finish. If you abort, the upgrade will not be applied."
381
+ lines << "When all conflicts are resolved, type /exit to finish. If you stop early, the files stay as they are (the rest of the upgrade is already applied)."
350
382
  lines.join("\n")
351
383
  end
352
384
 
@@ -503,7 +535,7 @@ if ARGV[0] == "bundle"
503
535
  ans = begin; $stdin.gets; rescue => _e; nil; end
504
536
  launch = ans && ans.strip.downcase.start_with?("y")
505
537
  else
506
- puts "Non-interactive terminal — aborting. Re-run with --force to overwrite or --agent in a TTY."
538
+ puts "Non-interactive terminal: kept your edits in the file(s) above; the rest is upgraded. Re-run with --force to take the bundle's version, or --agent in a TTY to merge."
507
539
  exit 2
508
540
  end
509
541
  if launch
@@ -511,37 +543,15 @@ if ARGV[0] == "bundle"
511
543
  puts "Launching interactive session for conflict resolution… (/exit when done)"
512
544
  require "samagotchi/terminal_ui"
513
545
  Samagotchi::TerminalUI.new(prompt: prompt).run
514
- # After session, re-check if conflicts resolved by re-reading current files vs incoming
515
- # Simple heuristic: if user edited files, current != base now but we assume resolved if file exists
516
- # Re-run upgrade without conflicts? User already edited in place, so we need to write provenance now
517
- # Update provenance to reflect resolved state
518
- if manifest
519
- # Re-collect provenance files from target dir for this bundle
520
- prov_files = {}
521
- target_scope = scope || provenance.read[:scope]&.to_s || "system"
522
- target_dir = Samagotchi::MemoryBundle::Installer.new(source: expanded_source, name: bundle_name, scope: target_scope).send(:resolve_target_dir, target_scope)
523
- # Actually reuse provenance write with current target files for bundle keys
524
- bundle_files = installer.conflicts.keys + installer.results.select { |_, r| %w[installed updated].include?(r[:status]) }.keys
525
- # Fallback: collect all md that exist and were in installer results
526
- all_keys = installer.results.keys
527
- all_keys.each do |k|
528
- p = File.join(target_dir, k)
529
- prov_files[k] = p if File.exist?(p)
530
- end
531
- if prov_files.any?
532
- Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).write(
533
- files: prov_files,
534
- scope: target_scope,
535
- version: manifest.version,
536
- source_path: expanded_source
537
- )
538
- puts "Provenance updated after interactive resolution."
539
- end
540
- end
546
+ # The installer already recorded the upgrade (conflicted files
547
+ # kept their old base); the resolved files now start from the
548
+ # bundle's version.
549
+ Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(installer.conflicts)
550
+ puts "Provenance updated after interactive resolution."
541
551
  puts "Upgrade resolved interactively."
542
552
  exit 0
543
553
  else
544
- puts "Upgrade aborted due to conflicts. Resolve manually or re-run with --force."
554
+ puts "Kept your edits in the file(s) above; the rest is upgraded. chi bundle diff #{bundle_name} FILE shows the base; re-run with --force to take the bundle's version."
545
555
  exit 2
546
556
  end
547
557
  end
@@ -611,6 +621,7 @@ if ARGV[0] == "bundle"
611
621
  puts "Target: #{st[:target_dir]}"
612
622
  st[:files].each do |k, info|
613
623
  mods = []
624
+ mods << "conflict (kept your edits over v#{st[:provenance][:version]}: chi bundle diff #{name} #{k})" if info[:conflict]
614
625
  mods << "modified" if info[:modified]
615
626
  mods << "missing" if info[:missing]
616
627
  mods << "no-index" unless info[:index_present]
@@ -780,6 +791,7 @@ if ARGV[0] == "bundle"
780
791
  line += " (shipped v#{b.upgrade.version}: chi bundle upgrade #{b.upgrade.source})" if b.upgrade
781
792
  puts line
782
793
  end
794
+ puts " (or all at once: chi update)" if installed.any?(&:upgrade)
783
795
  end
784
796
  unless available.empty?
785
797
  puts ""
@@ -881,7 +893,13 @@ if ARGV[0] == "bundle"
881
893
  exit 0
882
894
  end
883
895
 
884
- options = { verbose: false, no_interrupt: false, non_interactive: false, no_default_input: false, web: false, web_port: nil, web_open: false, web_markdown: nil, web_turn_view: nil }
896
+ # `chi scratch`: the run options below, for a session that leaves nothing
897
+ # behind (TerminalUI scratch:). The word goes before OptionParser sees it.
898
+ scratch = ARGV[0] == "scratch"
899
+ ARGV.shift if scratch
900
+
901
+ # no_interrupt / no_default_input start from config.yml or the env; the flags below turn them on.
902
+ options = { verbose: false, no_interrupt: Samagotchi::Config.get("no_interrupt"), non_interactive: false, no_default_input: Samagotchi::Config.get("no_default_input"), web: false, web_port: nil, web_open: false, web_markdown: nil, web_turn_view: nil }
885
903
  cli_overrides = {}
886
904
 
887
905
  if ARGV.any? { |arg| arg == "--backend" || arg.start_with?("--backend=") }
@@ -898,7 +916,7 @@ ARGV.each do |arg|
898
916
  base = arg.split("=", 2).first
899
917
  # If dashed version is a known flag (registry or hardcoded), reject underscore variant
900
918
  dashed = base.tr("_", "-")
901
- known = %w[--prompt --resume --attach --shared --no-shared --model --memory --mute --no-interrupt --non-interactive --verbose --no-default-input --port --open] + Samagotchi::Config.cli_entries.map(&:cli_flag)
919
+ known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --memory --mute --no-interrupt --non-interactive --verbose --no-default-input --port --open] + Samagotchi::Config.cli_entries.map(&:cli_flag)
902
920
  if known.include?(dashed) || known.include?(base)
903
921
  warn "Unknown option: #{arg} (did you mean #{dashed}?)"
904
922
  exit 1
@@ -908,13 +926,16 @@ end
908
926
  parser = OptionParser.new do |opts|
909
927
  opts.banner = <<~BANNER
910
928
  Usage: chi [options]
929
+ chi bootstrap [HOST[:PORT]|URL] first setup: find the model server, write config.yml
930
+ chi scratch [options] a one-time session in this terminal: nothing is kept
911
931
  chi web [--port PORT] [--open] [--scope=all] the web UI
912
- chi sessions <list|stop|delete|prune|clean>
932
+ chi sessions <list|stop|archive|unarchive|delete|prune|clean>
913
933
  chi note [--source NAME] [-m TEXT] ID...|--all background context for sessions
914
- chi send [-m TEXT] ID... a message into sessions, as if typed there
934
+ chi send [-m TEXT] ID...|--new a message into sessions, as if typed there
915
935
  chi bundle <install|upgrade|uninstall|status|diff|list|build>
916
936
  chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
917
937
  chi self version, paths, model and bundles
938
+ chi update [--dry-run] update this chi, its bundles and the desktop helper
918
939
  (each subcommand takes --help)
919
940
 
920
941
  BANNER
@@ -925,6 +946,8 @@ parser = OptionParser.new do |opts|
925
946
  opts.on("--model NAME", "Model selector (runtime only, overrides --resume) [alias for --default-model]") { |m| options[:model] = m; cli_overrides["default.model"] = m }
926
947
  opts.on("--profile NAME", Samagotchi::ModelProfile::NAMES,
927
948
  "Prompt profile for every model in this run (#{Samagotchi::ModelProfile::NAMES.join('|')}) [alias for --model-profile]") { |p| cli_overrides["model.profile"] = p }
949
+ opts.on("--thinking LEVEL", Samagotchi::Thinking::LEVELS.map(&:to_s), "How much the model thinks this run: off|low|medium|high|default (outranks config.yml; " \
950
+ "sessions already running keep theirs) [env SAMAGOTCHI_THINKING_LEVEL]") { |l| cli_overrides["thinking.level"] = l }
928
951
  opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
929
952
  opts.on("--mute NAME", "Hide a memory from this session: not in the prompt, refused by memory_read (repeatable; a comma list too)") { |m| (options[:muted] ||= []) << m }
930
953
  opts.on("--no-interrupt", "Raise the tool call limit to 1000 iterations (useful for long tasks)") { options[:no_interrupt] = true; cli_overrides["no_interrupt"] = true }
@@ -938,7 +961,8 @@ parser = OptionParser.new do |opts|
938
961
  "every session outside a repo) or all") { |v| options[:scope] = v }
939
962
  # Accepted values of string entries that the registry doesn't type as enums.
940
963
  value_hints = { "thinking.ui" => "spinner|off", "status.line" => "on|off", "status.width_mode" => "terminal_cap|fixed",
941
- "web.host" => "127.0.0.1|::1|localhost", "recap.sentences" => "N|N-M" }
964
+ "web.host" => "127.0.0.1|::1|localhost", "recap.sentences" => "N|N-M",
965
+ "web.annotate_presets" => "text|text" }
942
966
  # Universal config flags (implicit convention: ENV SAMAGOTCHI_* ↔ YAML dotted ↔ CLI --kebab)
943
967
  # Generated from Samagotchi::Config registry — Option A: snake leaf in YAML, kebab in CLI.
944
968
  Samagotchi::Config.cli_entries.each do |entry|
@@ -981,12 +1005,22 @@ end
981
1005
  # The subcommands with their own parsers ran above; what parse! leaves is
982
1006
  # `web` or nothing. Anything else is a typo, not a prompt: refuse it rather
983
1007
  # than start a session.
984
- stray = ARGV.first == "web" ? ARGV[1] : ARGV.first
1008
+ web = ARGV.first == "web" && !scratch
1009
+ stray = web ? ARGV[1] : ARGV.first
985
1010
  if stray
986
- warn "Error: #{ARGV.first == "web" ? "unexpected argument" : "unknown command"} #{stray} (see chi --help)"
1011
+ warn "Error: #{web || scratch ? "unexpected argument" : "unknown command"} #{stray} (see chi --help)"
987
1012
  exit 1
988
1013
  end
989
1014
 
1015
+ # A scratch session is new, runs here and goes when it ends.
1016
+ if scratch
1017
+ flag = { resume: "--resume", attach: "--attach", shared: "--shared" }.find { |key, _flag| options[key] }&.last
1018
+ if flag
1019
+ warn "Error: chi scratch starts a new session in this terminal and keeps nothing; it can't take #{flag}"
1020
+ exit 1
1021
+ end
1022
+ end
1023
+
990
1024
  # Apply CLI precedence: CLI > ENV > file
991
1025
  unless cli_overrides.empty?
992
1026
  Samagotchi::Config.reload!(cli_overrides: cli_overrides)
@@ -1029,7 +1063,18 @@ when "web"
1029
1063
  end
1030
1064
  end
1031
1065
 
1066
+ # The first interactive start of a new installed chi says what `chi update` would update.
1067
+ chi_update_hint = lambda do
1068
+ require "samagotchi/installed_gem"
1069
+ require "samagotchi/update_hint"
1070
+ if Samagotchi::UpdateHint.wanted?(prompt: options[:prompt], non_interactive: options[:non_interactive],
1071
+ tty: $stdin.tty? && $stdout.tty?, installed: Samagotchi::InstalledGem.spec)
1072
+ Samagotchi::UpdateHint.show
1073
+ end
1074
+ end
1075
+
1032
1076
  if options[:web]
1077
+ chi_update_hint.call
1033
1078
  require "samagotchi/web/server"
1034
1079
  port = options[:web_port] || Samagotchi::Config.get("web.port") || 4567
1035
1080
  # legacy env fallback
@@ -1039,7 +1084,8 @@ if options[:web]
1039
1084
  turn_view = options[:web_turn_view]
1040
1085
  turn_view = Samagotchi::Config.get("web.turn_view") if turn_view.nil?
1041
1086
  exit Samagotchi::Web::Server.launch(port: port, scope: options[:scope] || "project", open_browser: options[:web_open],
1042
- markdown: markdown, turn_view: turn_view)
1087
+ markdown: markdown, turn_view: turn_view,
1088
+ annotate_presets: Samagotchi::Config.get("web.annotate_presets"))
1043
1089
  end
1044
1090
 
1045
1091
  # --resume and --attach take a unique id prefix too, like git.
@@ -1050,6 +1096,22 @@ rescue Samagotchi::Session::AmbiguousId => e
1050
1096
  exit 1
1051
1097
  end
1052
1098
 
1099
+ # A `chi scratch` session a killed REPL left behind would turn into a kept
1100
+ # one if opened again; the next sweep deletes it.
1101
+ %i[resume attach].each do |key|
1102
+ next unless options[key]
1103
+
1104
+ leftover = begin
1105
+ Samagotchi::Session.load(options[key]).scratch
1106
+ rescue ArgumentError
1107
+ false # no such session: the launch says so
1108
+ end
1109
+ next unless leftover
1110
+
1111
+ warn "Error: that's a leftover scratch session; it is deleted at the next sweep (chi sessions clean)"
1112
+ exit 1
1113
+ end
1114
+
1053
1115
  # --memory and --mute names are checked here, before the launch: a worker's
1054
1116
  # stderr goes nowhere, so its engine's warnings would not be seen. Warnings
1055
1117
  # only; the session starts either way.
@@ -1081,8 +1143,13 @@ if options[:memories] || options[:muted]
1081
1143
  end
1082
1144
  end
1083
1145
 
1146
+ chi_update_hint.call
1084
1147
  require "samagotchi/launch_mode"
1085
- launch, launch_note = Samagotchi::LaunchMode.resolve(options, shared_config: Samagotchi::Config.get("session.shared"))
1148
+ launch, launch_note = if scratch
1149
+ [:repl, nil]
1150
+ else
1151
+ Samagotchi::LaunchMode.resolve(options, shared_config: Samagotchi::Config.get("session.shared"))
1152
+ end
1086
1153
  warn launch_note if launch_note
1087
1154
 
1088
1155
  if launch == :attached
@@ -1124,7 +1191,8 @@ begin
1124
1191
  memories: options[:memories] || [],
1125
1192
  muted_memories: options[:muted] || [],
1126
1193
  non_interactive: options[:non_interactive],
1127
- model_name: options[:model]
1194
+ model_name: options[:model],
1195
+ scratch: scratch
1128
1196
  ).run
1129
1197
  rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel => e
1130
1198
  warn "Error: #{e.message}"