samagotchi 0.3.0 → 0.5.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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +162 -1
  3. data/README.md +29 -2
  4. data/bin/chi +60 -69
  5. data/docs/cli.md +211 -77
  6. data/docs/configuration.md +118 -21
  7. data/docs/desktop.md +39 -4
  8. data/docs/guardrails.md +11 -0
  9. data/docs/hooks.md +89 -7
  10. data/docs/memory.md +40 -0
  11. data/docs/plugins.md +50 -0
  12. data/docs/releasing.md +15 -12
  13. data/docs/sessions.md +20 -18
  14. data/lib/samagotchi/bootstrap/config_writer.rb +1 -2
  15. data/lib/samagotchi/bridge/sse_writer.rb +0 -3
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
  17. data/lib/samagotchi/bridge.rb +20 -12
  18. data/lib/samagotchi/bundles/skills/manifest.yml +10 -0
  19. data/lib/samagotchi/bundles/skills/plugin.rb +419 -0
  20. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +178 -5
  21. data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
  22. data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
  23. data/lib/samagotchi/bundles/system/config_modification_protocol.md +9 -10
  24. data/lib/samagotchi/bundles/system/delegated.md +6 -7
  25. data/lib/samagotchi/bundles/system/identity.md +5 -0
  26. data/lib/samagotchi/bundles/system/manifest.yml +6 -6
  27. data/lib/samagotchi/bundles/system/memory_guide.md +26 -0
  28. data/lib/samagotchi/bundles/system/self_map.md +2 -1
  29. data/lib/samagotchi/client.rb +25 -26
  30. data/lib/samagotchi/commands/registry.rb +8 -0
  31. data/lib/samagotchi/config.rb +97 -113
  32. data/lib/samagotchi/desktop/macos/App.swift +12 -8
  33. data/lib/samagotchi/desktop/macos/ChiRunner.swift +4 -2
  34. data/lib/samagotchi/desktop/macos/Images.swift +113 -0
  35. data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
  36. data/lib/samagotchi/desktop/macos/Panel.swift +112 -9
  37. data/lib/samagotchi/desktop/macos.rb +59 -8
  38. data/lib/samagotchi/desktop_command.rb +6 -3
  39. data/lib/samagotchi/edit_preview.rb +82 -0
  40. data/lib/samagotchi/engine.rb +236 -443
  41. data/lib/samagotchi/gem_update.rb +89 -0
  42. data/lib/samagotchi/guardrails/approval.rb +26 -4
  43. data/lib/samagotchi/guardrails/load_failures.rb +9 -3
  44. data/lib/samagotchi/host_registry.rb +8 -12
  45. data/lib/samagotchi/idle_client.rb +24 -15
  46. data/lib/samagotchi/idle_reminders.rb +2 -2
  47. data/lib/samagotchi/image_store.rb +10 -6
  48. data/lib/samagotchi/kernel_loop.rb +59 -123
  49. data/lib/samagotchi/live_versions.rb +65 -0
  50. data/lib/samagotchi/llm/api_key.rb +41 -0
  51. data/lib/samagotchi/llm/chat_loop.rb +77 -13
  52. data/lib/samagotchi/llm/errors.rb +38 -7
  53. data/lib/samagotchi/llm/http.rb +19 -22
  54. data/lib/samagotchi/llm/openai_chat.rb +22 -26
  55. data/lib/samagotchi/memory_bundle/installer.rb +65 -63
  56. data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
  57. data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
  58. data/lib/samagotchi/memory_bundle/status.rb +4 -1
  59. data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
  60. data/lib/samagotchi/model_profile.rb +27 -10
  61. data/lib/samagotchi/note_command.rb +2 -1
  62. data/lib/samagotchi/prompt.rb +4 -2
  63. data/lib/samagotchi/reminder_store.rb +1 -9
  64. data/lib/samagotchi/reply_wait.rb +48 -4
  65. data/lib/samagotchi/self_report.rb +37 -5
  66. data/lib/samagotchi/send_command.rb +190 -17
  67. data/lib/samagotchi/session.rb +4 -2
  68. data/lib/samagotchi/session_commands.rb +38 -8
  69. data/lib/samagotchi/session_manager.rb +19 -53
  70. data/lib/samagotchi/system_prompt.rb +403 -0
  71. data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -3
  72. data/lib/samagotchi/terminal_ui/attached_loop.rb +141 -108
  73. data/lib/samagotchi/terminal_ui/attached_view.rb +27 -12
  74. data/lib/samagotchi/terminal_ui/event_renderer.rb +29 -8
  75. data/lib/samagotchi/terminal_ui/formatting.rb +41 -22
  76. data/lib/samagotchi/terminal_ui/input_support.rb +7 -23
  77. data/lib/samagotchi/terminal_ui/plain_surface.rb +13 -7
  78. data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
  79. data/lib/samagotchi/terminal_ui/status_row.rb +81 -0
  80. data/lib/samagotchi/terminal_ui/surface.rb +1 -1
  81. data/lib/samagotchi/terminal_ui.rb +142 -923
  82. data/lib/samagotchi/text_diff.rb +181 -0
  83. data/lib/samagotchi/thinking.rb +126 -0
  84. data/lib/samagotchi/tool_activity.rb +52 -2
  85. data/lib/samagotchi/tool_runner.rb +37 -1
  86. data/lib/samagotchi/tools/ask_user_question.rb +41 -33
  87. data/lib/samagotchi/tools/edit.rb +23 -9
  88. data/lib/samagotchi/tools/execute.rb +3 -3
  89. data/lib/samagotchi/tools/output_guardrails.rb +8 -7
  90. data/lib/samagotchi/tools/read.rb +4 -4
  91. data/lib/samagotchi/tools/write.rb +4 -0
  92. data/lib/samagotchi/turn_flow.rb +12 -2
  93. data/lib/samagotchi/update_command.rb +309 -0
  94. data/lib/samagotchi/update_hint.rb +59 -0
  95. data/lib/samagotchi/version.rb +1 -1
  96. data/lib/samagotchi/vision_support.rb +6 -4
  97. data/lib/samagotchi/web/app.rb +173 -38
  98. data/lib/samagotchi/web/lan.rb +99 -0
  99. data/lib/samagotchi/web/message_parts.rb +19 -10
  100. data/lib/samagotchi/web/public/activity.js +10 -0
  101. data/lib/samagotchi/web/public/app.js +135 -78
  102. data/lib/samagotchi/web/public/chat_view.js +8 -1
  103. data/lib/samagotchi/web/public/data.js +2 -0
  104. data/lib/samagotchi/web/public/diff_view.js +58 -0
  105. data/lib/samagotchi/web/public/index.html +185 -18
  106. data/lib/samagotchi/web/public/model_pick.js +136 -0
  107. data/lib/samagotchi/web/public/model_picker.js +224 -0
  108. data/lib/samagotchi/web/public/notify.js +10 -0
  109. data/lib/samagotchi/web/public/question_card.js +3 -1
  110. data/lib/samagotchi/web/public/stage_model.js +110 -0
  111. data/lib/samagotchi/web/public/stage_view.js +580 -0
  112. data/lib/samagotchi/web/public/timing.js +6 -2
  113. data/lib/samagotchi/web/public/turn_events.js +38 -10
  114. data/lib/samagotchi/web/public/turn_model.js +11 -3
  115. data/lib/samagotchi/web/public/turn_view.js +76 -20
  116. data/lib/samagotchi/web/qr.rb +40 -0
  117. data/lib/samagotchi/web/server.rb +101 -11
  118. data/lib/samagotchi/web/token.rb +97 -0
  119. data/lib/samagotchi/worker.rb +5 -4
  120. metadata +38 -3
  121. data/lib/samagotchi/terminal_ui/legacy_surface.rb +0 -111
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 950fbee37d2110efad37f8d3f8981144f49af9a2ccf75649d5953b1fe7773260
4
- data.tar.gz: aaad7adb0e9aa3c81b601df48fde4ebab965862ed954454c8b79e2de841f0789
3
+ metadata.gz: 94cd11abcfad07ad2843b4e0fc2eea368d77ab6080403d308db906270a1832c6
4
+ data.tar.gz: 21ac6e7093ee196d98e2ba50979cdbb28f1a3a10ab0e9cac9bad01c88a078daf
5
5
  SHA512:
6
- metadata.gz: 1b4d092f263192c242dead3ae0c556758a38af2f01285dc590362d698696a31d38801c70e64ff85daa33067ceda92be7687915024d1e81802bd0a2c365cc292d
7
- data.tar.gz: 7a06a4d47dea9168e40b0ae0ba7bc6cbfe1b377ba9ac29a998d19f0ee17a766de3186786894deea8b6765a78e21b14e3e7ece8238ab401354c43a5eba4d1815b
6
+ metadata.gz: fa39259864928d70d8cf830d0c7cc15c6865cea75d383484979897800a9f79d6f9fa4d79df3700285d9871e8d0cf5420e9af7c0143ab08e71182befa287b52af
7
+ data.tar.gz: 24f0fe6863767b628c1eec6957bf6cf33d8966b45f8b2cbdfd7ab268d6c323835dba8ff09fbdd9359775263693228a7a11f0d125cf0a38d7f044d6932ca7c4b9
data/CHANGELOG.md CHANGED
@@ -8,6 +8,165 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.5.0] - 2026-09-30
12
+
13
+ ### Security
14
+
15
+ - `chi web` and the worker Bridge refuse requests from other websites. Before,
16
+ a page open in your browser could start a session that runs commands, or
17
+ read your sessions and their output. Please update if you run `chi web`.
18
+
19
+ ### Added
20
+
21
+ - Skills: a memory named `skill_<name>` holds the steps of a task you did with
22
+ chi. Say "let's memorize this" and chi saves it; next time it reads and
23
+ follows it, and fixes a step that turned out different in the same turn
24
+ (docs/memory.md, Skills). The system bundle's identity and memory guide
25
+ teach it.
26
+ - The `skills` bundle (`chi bundle install skills`): `/skill save [name]
27
+ [--system]`, `/skill list`, `/skill show`, `/skill diff`; older versions of
28
+ each skill kept (`history_keep`) with a one-line diff after every update;
29
+ a nudge when a followed skill's step fails and the model goes on without
30
+ fixing it (docs/plugins.md, The skills bundle).
31
+ - `chi web --web-host lan` (`web.host: lan`, or one of this machine's IPv4
32
+ addresses): the web UI on your phone. chi web also listens on the LAN
33
+ address and prints a link with an access token and its QR code; every
34
+ request from another machine needs the token, which the page keeps in a
35
+ cookie. `chi web --new-token` replaces it; `chi self` says when chi web runs
36
+ on the LAN. Plain http: for a home network only. New runtime dependency:
37
+ `rqrcode_core`.
38
+ - A third web view, `web.view: stage` (`--web-view stage`): the running turn
39
+ sits above the composer, its tool calls in an expandable cloud, and hands
40
+ off smoothly into the history when it ends. `web.view: turn | stage | chat`
41
+ replaces `web.turn_view` (turn stays the default).
42
+ - The start page's model picker is searchable: models grouped by host, your
43
+ last 5 picks first, every typed word matched ("deepseek4.1 fla" finds
44
+ `openrouter · deepseek/deepseek-v4.1-flash`), arrow keys, Enter and Esc.
45
+
46
+ ### Changed
47
+
48
+ - config.yml edits apply without restarting `chi web`: new sessions read the
49
+ file, and running ones pick up settings they read each time (retries,
50
+ limits, log level). Old flat `UPPER_CASE` keys and `backend:` are no longer
51
+ read; they warn as unknown keys.
52
+ - The terminal REPL (`chi --no-shared`, `chi scratch`) shows turns the way
53
+ the attached terminal does, and all three UIs end a turn with the same
54
+ words ("✕ turn canceled (Ctrl-C) · 3.1s", "✕ turn failed: …"); a provider
55
+ retry shows as a dim line. The REPL-only settings `thinking.ui`,
56
+ `thinking.render_interval` and `status.width_mode` / `max_width` /
57
+ `fixed_width` are gone.
58
+ - `session.keep_status` defaults to none: a session left "running" by a
59
+ crashed worker is cleaned up like any other; a session with a live worker
60
+ is never removed.
61
+ - `chi send --image` to a model that can't take images is refused before
62
+ anything is sent, with the reason, instead of failing in the session.
63
+ - A low/medium/high thinking level on a llama.cpp chat host whose template
64
+ ignores it says so once.
65
+ - The web answers `/stats`, `/recap` and `/detach` itself (they're terminal
66
+ commands) instead of showing a worker error.
67
+ - Piped input to an attached chi (`echo "do X" | chi`, `chi -p X` with no
68
+ terminal) waits for its turns to end and exits 1 if one failed, instead of
69
+ detaching at once.
70
+
71
+ ### Fixed
72
+
73
+ - OpenRouter's "overloaded" error sent inside an HTTP 200 is retried like a
74
+ 503 instead of failing the turn.
75
+ - The context line the model sees during a long tool loop counts the tool
76
+ output added since the last request, so the model knows when to wrap up.
77
+ - `chi send --wait` no longer hangs when the turn fails very fast (for
78
+ example right after an earlier failure); it says the turn failed and why.
79
+ - On Linux the system prompt now gets the "prefer rg" hint when rg is
80
+ installed (the check only worked on macOS).
81
+ - `chi sessions list --live` shows the test sessions of a test run.
82
+ - `chi -p` with no terminal (stdin from a pipe or /dev/null) sends its prompt
83
+ once: a failed turn re-sent it in a loop (hundreds of requests a second),
84
+ and an attached `chi -p` didn't send it at all.
85
+
86
+ Update with `chi update`. Bundle added: skills 0.1.0 (`chi bundle install
87
+ skills`).
88
+
89
+ ## [0.4.0] - 2026-09-30
90
+
91
+ ### Added
92
+
93
+ - `chi update`: brings an installed chi up to date in one command: the gem
94
+ from rubygems, then the system bundle, the shipped bundles you installed and
95
+ the desktop helper (rebuilt only when its sources changed), shown in one
96
+ table. Your edits to bundle files are kept and reported, running workers and
97
+ an old `chi web` are only reported, `--dry-run` shows the plan, and
98
+ `--no-gem` / `--no-bundles` / `--no-desktop` (or `update.*` in config.yml)
99
+ skip parts. The first start of a new chi version says in one line when
100
+ something can be updated.
101
+ - A thinking level per model, host or run: `thinking: off | low | medium |
102
+ high | default` (`models.<key>.thinking`, `hosts.<name>.thinking`,
103
+ `thinking.level`, `--thinking LEVEL`). Chat hosts get the matching request
104
+ fields; native Qwen and Gemma turn thinking off. chi says once when a level
105
+ can't work on a host, and a host that refuses the thinking fields (gpt-oss
106
+ on OpenRouter) is asked again without them. `/model` and `chi self` show
107
+ the level.
108
+ - Edit previews: an edit/write approval shows the diff it would make, in the
109
+ web card and in the terminal, and every edit/write row gets a
110
+ "diff +3 −1" that opens to the change (live, after a reload and on a join).
111
+ - `chi send --image PATH` (repeatable, up to 20): images go with the message
112
+ as attachments, the same as the web composer's chips; converted and
113
+ downscaled once, then copied into each session. Works with `--new` (the
114
+ session starts idle, then gets the message with its images) and `--wait`.
115
+ A missing file or a non-image stops the send before anything goes out.
116
+ - The desktop helper takes images: a clipboard screenshot, image files from
117
+ Finder's Send to chi, other apps' image data, or a drop on the panel; they
118
+ show as thumbnails and go with the message.
119
+ - source-links 0.3.0: `#12` links to the current project's repo (from its git
120
+ remote) and `owner/repo#12` to that repo; `url:` templates take `{1}`,
121
+ `{name}`, `{repo}` and `{host}`, with `remote:` and `remote_host:` per
122
+ source, and the note lists each link once.
123
+ - The web shows when the provider is asked again after an error ("↻ retrying
124
+ (503) in 3 s, 1/2") or a turn waits for a plugin's setup.
125
+
126
+ ### Changed
127
+
128
+ - A model named with a host that isn't configured (`nosuch:org/model`, or a
129
+ provider's name like `openrouter:x`) is an error that names the host and
130
+ lists the configured ones, in the CLI, `chi send --new`, the web and
131
+ `/model`, instead of going to the default host.
132
+ - The terminal no longer rewrites `#word` into a memory reference: "PR #1"
133
+ and "#ff0000" reach the model as typed (Tab still completes `#name`).
134
+ - A question you dismiss reaches the model as dismissed, not as a tool
135
+ error, so it keeps asking when it should.
136
+ - `/quit` works in the REPL like `/exit`, and the web answers it too;
137
+ `/stats` and `/recap` take trailing words in both terminals.
138
+ - A delegated session gets reworded rules: the answer first, then evidence,
139
+ then what's unverified; follow-up messages arrive as new turns.
140
+
141
+ ### Fixed
142
+
143
+ - A llama.cpp server started with `--api-key` works: the key in
144
+ `api_key_env` is sent on every request (it got 401). A 401/403 from any host
145
+ now says which variable to check or to set `api_key_env`.
146
+ - A bundle upgrade that keeps your edited file no longer disables the
147
+ bundle's plugin, and the system bundle stops warning about the same edit on
148
+ every start.
149
+ - One bundle with a broken manifest.json no longer stops the hooks of the
150
+ bundles after it from loading.
151
+ - A long-running worker picks up edited guardrail rules.
152
+ - `write` without content fails instead of emptying the file.
153
+ - Per-model `vision:` follows a model alias, like `profile:` and `sampling:`.
154
+ - A timeout of 0 in config means the default on llama.cpp hosts too (it was a
155
+ 0-second timeout).
156
+ - A reminder turn drops a pending continue offer, so a later "no" can't roll
157
+ the reminder back.
158
+ - More than 8 options in `ask_user_question` is always an error (one path
159
+ silently used the first 8).
160
+ - A memory read with a comma list counts each name in the REPL and the web.
161
+ - `/archive` or `/quit` typed during a REPL turn no longer goes to the model
162
+ as text; `EXIT --DELETE` works in any case in an attached terminal.
163
+ - `chi bundle status` finds index lines again (it said "no-index" for every
164
+ file).
165
+
166
+ Update with `chi update` (new in this version: from 0.3.0, run
167
+ `gem install samagotchi` once, then `chi update`). Bundle moved:
168
+ source-links 0.3.0.
169
+
11
170
  ## [0.3.0] - 2026-09-29
12
171
 
13
172
  ### Added
@@ -153,6 +312,8 @@ and long-lived sessions.
153
312
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
154
313
  hotkey panel that send selected text or the clipboard to your sessions.
155
314
 
156
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.3.0...HEAD
315
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.5.0...HEAD
316
+ [0.5.0]: https://github.com/dm1try/samagotchi/compare/v0.4.0...v0.5.0
317
+ [0.4.0]: https://github.com/dm1try/samagotchi/compare/v0.3.0...v0.4.0
157
318
  [0.3.0]: https://github.com/dm1try/samagotchi/compare/v0.2.0...v0.3.0
158
319
  [0.2.0]: https://github.com/dm1try/samagotchi/releases/tag/v0.2.0
data/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # samagotchi
2
2
 
3
- An agent harness that relies heavily on memory. Samagotchi is the engine; chi
4
- (pronounced "chee") is its short name and CLI command.
3
+ chi is a local-first, human-in-the-loop agent harness. It remembers what you
4
+ teach it, as memories and skills, and corrects them when a step turns out
5
+ different. You stay in the loop from the terminal or the browser.
6
+
7
+ Samagotchi is the engine; chi (pronounced "chee") is its short name and CLI
8
+ command.
5
9
 
6
10
  > **Pre-1.0:** config and commands may change between minor versions (0.2 →
7
11
  > 0.3); the [CHANGELOG](CHANGELOG.md) says what changed. chi is used daily and
@@ -32,6 +36,22 @@ bin/chi self
32
36
  `bin/chi` runs the checkout; `bundle exec rake gem:install` installs it as a
33
37
  local gem, which puts `chi` on your PATH.
34
38
 
39
+ ### Updating
40
+
41
+ ```sh
42
+ chi update --dry-run # what would change
43
+ chi update # the gem, then the bundles and the desktop helper
44
+ ```
45
+
46
+ `chi update` installs the newest samagotchi from rubygems.org (old versions
47
+ stay installed: running sessions still use them), then brings the rest up to
48
+ the new version: the system bundle, the bundles chi ships that you installed,
49
+ and the macOS helper (rebuilt only when its sources changed). Memory files you
50
+ edited are kept, and the table says which. Running sessions move to the new
51
+ chi when their worker idles out (30 min) or on `chi sessions stop ID`; a
52
+ running `chi web` needs a restart. From a checkout, `git pull` instead. See
53
+ [CLI: Updating](docs/cli.md#updating).
54
+
35
55
  ## Set up
36
56
 
37
57
  Point chi at your model server; it works out the rest and writes
@@ -87,6 +107,7 @@ chi --no-shared # the plain in-process REPL
87
107
  chi -p "explain lib/" --non-interactive # one turn, print the answer, exit
88
108
  chi --resume <session-id> # continue a saved session
89
109
  chi web --open # web UI: this project's sessions (--scope=all: every one)
110
+ chi web --web-host lan # the web UI on your phone too: scan the QR code it prints
90
111
  chi sessions list # this project's saved sessions (--scope=all: every one)
91
112
  pbpaste | chi note --source slack <id> # background context for a session (no turn)
92
113
  pbpaste | chi send -m "same bug?" <id> # a message to a session, the clipboard quoted above it
@@ -97,6 +118,12 @@ chi send --new --wait -m "review feat/x" # a new session you can watch in the w
97
118
 
98
119
  `/model` switches models, Ctrl-C cancels a turn, and Ctrl-D or `/detach` detaches (the session keeps running; `chi --attach ID` comes back). `/exit` detaches and stops the session's worker too, unless something still needs it (a running turn, another UI); `chi --resume ID` picks the conversation up again. `/exit --delete` also deletes the session once the worker has gone; `chi sessions delete ID` deletes one from the shell. `/archive` (or `chi sessions archive ID`) hides a session from every list and keeps it for good; `chi sessions list --archived` finds it again.
99
120
 
121
+ `chi web --web-host lan` opens the web UI to your home network with an
122
+ access token: anyone with the printed link (or its QR code) can run commands
123
+ as you, and it is plain http, readable by anyone on the same Wi-Fi. Use it at
124
+ home, never on a shared network, and `chi web --new-token` if a link leaks.
125
+ See [chi web on your phone](docs/cli.md#chi-web-on-your-phone).
126
+
100
127
  ### Context notes
101
128
 
102
129
  `chi note` pushes text into one or more sessions as background, not as a
data/bin/chi CHANGED
@@ -14,7 +14,7 @@ end
14
14
  require "optparse"
15
15
  require "samagotchi/config"
16
16
 
17
- Samagotchi::ConfigFile.load_global_env!
17
+ Samagotchi::ConfigFile.load!
18
18
  require "samagotchi"
19
19
 
20
20
  # `chi self`: print version, source dir, config/memory/session paths, model and
@@ -50,7 +50,7 @@ if ARGV[0] == "sessions"
50
50
  puts " delete [--force] ID... # delete sessions for good (IDs or unique prefixes); --force stops a live worker first"
51
51
  puts " prune [--dry-run] [--days N] [--keep N] [--keep-status running,...] [--test-only]"
52
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"
53
- puts "Defaults: days=14 keep=500 keep_status=running (env overrides: SAMAGOTCHI_SESSION_RETENTION_DAYS etc.)"
53
+ puts "Defaults: days=14 keep=500 keep_status=none (config: session.retention_days, session.max_count, session.keep_status)"
54
54
  exit 0
55
55
  end
56
56
  dry_run = sessions_args.include?("--dry-run")
@@ -110,10 +110,12 @@ if ARGV[0] == "sessions"
110
110
  project = scope == "all" || cwd ? nil : Samagotchi::ProjectScope.root_for(Dir.pwd)
111
111
  scope_note = ->(count) { "#{count} session(s) in #{File.basename(project)} (--scope=all: every project)" }
112
112
  # The picker (chi note from a script): filters apply before the limit,
113
- # and test runs stay out.
113
+ # and test runs stay out, unless this is one (SAMAGOTCHI_ENV=test, CI):
114
+ # then its own sessions are what it looks for.
114
115
  if live || cwd || (format && format != "text")
115
116
  summaries = Samagotchi::SessionManager.session_summaries(
116
- live: live, cwd: cwd, limit: limit || (live ? 10 : nil), include_tests: false, project_root: project,
117
+ live: live, cwd: cwd, limit: limit || (live ? 10 : nil), include_tests: Samagotchi::Session.test_session_env?,
118
+ project_root: project,
117
119
  include_archived: include_archived
118
120
  )
119
121
  case format
@@ -142,7 +144,7 @@ if ARGV[0] == "sessions"
142
144
  end
143
145
  # A delegated session points at its parent.
144
146
  child = summary[:parent_short_id] ? " ↳ #{summary[:parent_short_id]}" : ""
145
- flag = summary[:scratch] ? " [scratch]" : ""
147
+ flag = summary[:scratch] ? " [scratch]" : (summary[:test_run] ? " [test]" : "")
146
148
  flag += " [archived]" if summary[:archived]
147
149
  ctx = Samagotchi::SessionMetrics.context_label(summary[:ctx_pct])
148
150
  puts "#{summary[:id]} #{state.ljust(8)} #{ctx.ljust(8)} #{summary[:updated_at]} #{desc}#{flag}#{child}"
@@ -257,6 +259,12 @@ if ARGV[0] == "bootstrap"
257
259
  exit Samagotchi::BootstrapCommand.new(ARGV[1..] || []).run
258
260
  end
259
261
 
262
+ # `chi update`: bring an installed chi up to date, before OptionParser (its own flags)
263
+ if ARGV[0] == "update"
264
+ require "samagotchi/update_command"
265
+ exit Samagotchi::UpdateCommand.new(ARGV[1..] || []).run
266
+ end
267
+
260
268
  # `chi desktop`: the native "Send to chi" helper, before OptionParser (its own flags)
261
269
  if ARGV[0] == "desktop"
262
270
  require "samagotchi/desktop_command"
@@ -372,7 +380,7 @@ if ARGV[0] == "bundle"
372
380
  lines << ""
373
381
  lines << "Your task: resolve each conflict by editing the file in place using the memory_write tool."
374
382
  lines << "Use memory_read to inspect current content if needed. The correct scope for memory_write is the bundle's scope."
375
- lines << "When all conflicts are resolved, type /exit to finish. If you abort, the upgrade will not be applied."
383
+ 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)."
376
384
  lines.join("\n")
377
385
  end
378
386
 
@@ -529,7 +537,7 @@ if ARGV[0] == "bundle"
529
537
  ans = begin; $stdin.gets; rescue => _e; nil; end
530
538
  launch = ans && ans.strip.downcase.start_with?("y")
531
539
  else
532
- puts "Non-interactive terminal — aborting. Re-run with --force to overwrite or --agent in a TTY."
540
+ 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."
533
541
  exit 2
534
542
  end
535
543
  if launch
@@ -537,37 +545,15 @@ if ARGV[0] == "bundle"
537
545
  puts "Launching interactive session for conflict resolution… (/exit when done)"
538
546
  require "samagotchi/terminal_ui"
539
547
  Samagotchi::TerminalUI.new(prompt: prompt).run
540
- # After session, re-check if conflicts resolved by re-reading current files vs incoming
541
- # Simple heuristic: if user edited files, current != base now but we assume resolved if file exists
542
- # Re-run upgrade without conflicts? User already edited in place, so we need to write provenance now
543
- # Update provenance to reflect resolved state
544
- if manifest
545
- # Re-collect provenance files from target dir for this bundle
546
- prov_files = {}
547
- target_scope = scope || provenance.read[:scope]&.to_s || "system"
548
- target_dir = Samagotchi::MemoryBundle::Installer.new(source: expanded_source, name: bundle_name, scope: target_scope).send(:resolve_target_dir, target_scope)
549
- # Actually reuse provenance write with current target files for bundle keys
550
- bundle_files = installer.conflicts.keys + installer.results.select { |_, r| %w[installed updated].include?(r[:status]) }.keys
551
- # Fallback: collect all md that exist and were in installer results
552
- all_keys = installer.results.keys
553
- all_keys.each do |k|
554
- p = File.join(target_dir, k)
555
- prov_files[k] = p if File.exist?(p)
556
- end
557
- if prov_files.any?
558
- Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).write(
559
- files: prov_files,
560
- scope: target_scope,
561
- version: manifest.version,
562
- source_path: expanded_source
563
- )
564
- puts "Provenance updated after interactive resolution."
565
- end
566
- end
548
+ # The installer already recorded the upgrade (conflicted files
549
+ # kept their old base); the resolved files now start from the
550
+ # bundle's version.
551
+ Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(installer.conflicts)
552
+ puts "Provenance updated after interactive resolution."
567
553
  puts "Upgrade resolved interactively."
568
554
  exit 0
569
555
  else
570
- puts "Upgrade aborted due to conflicts. Resolve manually or re-run with --force."
556
+ 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."
571
557
  exit 2
572
558
  end
573
559
  end
@@ -637,6 +623,7 @@ if ARGV[0] == "bundle"
637
623
  puts "Target: #{st[:target_dir]}"
638
624
  st[:files].each do |k, info|
639
625
  mods = []
626
+ mods << "conflict (kept your edits over v#{st[:provenance][:version]}: chi bundle diff #{name} #{k})" if info[:conflict]
640
627
  mods << "modified" if info[:modified]
641
628
  mods << "missing" if info[:missing]
642
629
  mods << "no-index" unless info[:index_present]
@@ -806,6 +793,7 @@ if ARGV[0] == "bundle"
806
793
  line += " (shipped v#{b.upgrade.version}: chi bundle upgrade #{b.upgrade.source})" if b.upgrade
807
794
  puts line
808
795
  end
796
+ puts " (or all at once: chi update)" if installed.any?(&:upgrade)
809
797
  end
810
798
  unless available.empty?
811
799
  puts ""
@@ -913,15 +901,9 @@ scratch = ARGV[0] == "scratch"
913
901
  ARGV.shift if scratch
914
902
 
915
903
  # no_interrupt / no_default_input start from config.yml or the env; the flags below turn them on.
916
- 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 }
904
+ 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_view: nil }
917
905
  cli_overrides = {}
918
906
 
919
- if ARGV.any? { |arg| arg == "--backend" || arg.start_with?("--backend=") }
920
- warn "Error: --backend was removed. A host's api: in config.yml picks the loop " \
921
- "(api: openai for the chat API; see docs/configuration.md)."
922
- exit 1
923
- end
924
-
925
907
  # Strict kebab enforcement: --recap_base_url is unknown (OptionParser would otherwise accept _ as -)
926
908
  ARGV.each do |arg|
927
909
  next unless arg.start_with?("--") && arg.include?("_")
@@ -930,7 +912,7 @@ ARGV.each do |arg|
930
912
  base = arg.split("=", 2).first
931
913
  # If dashed version is a known flag (registry or hardcoded), reject underscore variant
932
914
  dashed = base.tr("_", "-")
933
- 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)
915
+ known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --memory --mute --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
934
916
  if known.include?(dashed) || known.include?(base)
935
917
  warn "Unknown option: #{arg} (did you mean #{dashed}?)"
936
918
  exit 1
@@ -949,6 +931,7 @@ parser = OptionParser.new do |opts|
949
931
  chi bundle <install|upgrade|uninstall|status|diff|list|build>
950
932
  chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
951
933
  chi self version, paths, model and bundles
934
+ chi update [--dry-run] update this chi, its bundles and the desktop helper
952
935
  (each subcommand takes --help)
953
936
 
954
937
  BANNER
@@ -959,6 +942,8 @@ parser = OptionParser.new do |opts|
959
942
  opts.on("--model NAME", "Model selector (runtime only, overrides --resume) [alias for --default-model]") { |m| options[:model] = m; cli_overrides["default.model"] = m }
960
943
  opts.on("--profile NAME", Samagotchi::ModelProfile::NAMES,
961
944
  "Prompt profile for every model in this run (#{Samagotchi::ModelProfile::NAMES.join('|')}) [alias for --model-profile]") { |p| cli_overrides["model.profile"] = p }
945
+ 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; " \
946
+ "sessions already running keep theirs) [env SAMAGOTCHI_THINKING_LEVEL]") { |l| cli_overrides["thinking.level"] = l }
962
947
  opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
963
948
  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 }
964
949
  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 }
@@ -967,12 +952,12 @@ parser = OptionParser.new do |opts|
967
952
  opts.on("--no-default-input", "Skip default REPL input prefill") { options[:no_default_input] = true; cli_overrides["no_default_input"] = true }
968
953
  opts.on("--port PORT", "Port for `chi web` (default: 4567, env SAMAGOTCHI_WEB_PORT)") { |p| options[:web_port] = p.to_i; cli_overrides["web.port"] = p.to_s }
969
954
  opts.on("--open", "Open browser for `chi web`") { options[:web_open] = true }
955
+ opts.on("--new-token", "`chi web`: replace the LAN access token (web.host: lan); links and QR codes made with the old one stop working") { options[:new_token] = true }
970
956
  opts.on("--scope VALUE", %w[project all],
971
957
  "Which sessions `chi web` and `chi sessions list` show: project (the default: this folder's git project; " \
972
958
  "every session outside a repo) or all") { |v| options[:scope] = v }
973
959
  # Accepted values of string entries that the registry doesn't type as enums.
974
- value_hints = { "thinking.ui" => "spinner|off", "status.line" => "on|off", "status.width_mode" => "terminal_cap|fixed",
975
- "web.host" => "127.0.0.1|::1|localhost", "recap.sentences" => "N|N-M",
960
+ value_hints = { "status.line" => "on|off", "web.host" => "127.0.0.1|::1|localhost|lan|IP", "recap.sentences" => "N|N-M",
976
961
  "web.annotate_presets" => "text|text" }
977
962
  # Universal config flags (implicit convention: ENV SAMAGOTCHI_* ↔ YAML dotted ↔ CLI --kebab)
978
963
  # Generated from Samagotchi::Config registry — Option A: snake leaf in YAML, kebab in CLI.
@@ -980,7 +965,7 @@ parser = OptionParser.new do |opts|
980
965
  next if %w[no_interrupt no_default_input].include?(entry.key)
981
966
  next if entry.key == "web.port" # already handled as --port
982
967
  next if entry.key == "default.model" # alias --model above
983
- next if %w[web.markdown web.turn_view].include?(entry.key) # handled below with explicit inverse flags
968
+ next if entry.key == "web.markdown" # handled below with explicit inverse flags
984
969
  case entry.type
985
970
  when :bool
986
971
  # --[no-]: a bool that defaults to on is only useful switched off.
@@ -1000,8 +985,6 @@ parser = OptionParser.new do |opts|
1000
985
  end
1001
986
  opts.on("--web-markdown", "Render finalized LLM Markdown in `chi web` (requires optional commonmarker gem)") { options[:web_markdown] = true; cli_overrides["web.markdown"] = true }
1002
987
  opts.on("--no-web-markdown", "Disable Markdown rendering in `chi web`") { options[:web_markdown] = false; cli_overrides["web.markdown"] = false }
1003
- opts.on("--web-turn-view", "Show each turn in `chi web` as one block of steps (the live one at the bottom; the default); ?view=turn|chat overrides per page") { options[:web_turn_view] = true; cli_overrides["web.turn_view"] = true }
1004
- opts.on("--no-web-turn-view", "Show turns in `chi web` as the classic row of bubbles instead of the turn view") { options[:web_turn_view] = false; cli_overrides["web.turn_view"] = false }
1005
988
  # Strict underscore rejection: --recap_base_url etc. are not registered and will raise OptionParser::InvalidOption
1006
989
  opts.on("--version", "Print chi's version") { puts "chi #{Samagotchi::VERSION}"; exit }
1007
990
  opts.on("-h", "--help", "Show help") { puts opts; exit }
@@ -1022,6 +1005,10 @@ if stray
1022
1005
  warn "Error: #{web || scratch ? "unexpected argument" : "unknown command"} #{stray} (see chi --help)"
1023
1006
  exit 1
1024
1007
  end
1008
+ if options[:new_token] && !web
1009
+ warn "Error: --new-token goes with chi web (chi web --new-token)"
1010
+ exit 1
1011
+ end
1025
1012
 
1026
1013
  # A scratch session is new, runs here and goes when it ends.
1027
1014
  if scratch
@@ -1032,16 +1019,9 @@ if scratch
1032
1019
  end
1033
1020
  end
1034
1021
 
1035
- # Apply CLI precedence: CLI > ENV > file
1036
- unless cli_overrides.empty?
1037
- Samagotchi::Config.reload!(cli_overrides: cli_overrides)
1038
- # Keep ENV in sync for legacy readers still using ENV directly
1039
- cli_overrides.each do |k, v|
1040
- entry = Samagotchi::Config.find_by_key(k)
1041
- next unless entry&.env_exposed?
1042
- ENV[entry.env_key] = v.to_s
1043
- end
1044
- end
1022
+ # Apply CLI precedence: CLI > ENV > file. Workers this chi spawns get
1023
+ # these through their env (Config.cli_env).
1024
+ Samagotchi::Config.reload!(cli_overrides: cli_overrides) unless cli_overrides.empty?
1045
1025
 
1046
1026
  # The debug log (Samagotchi::LogPath, level log.level) for every command.
1047
1027
  # -v (the plain REPL only): debug records, each mirrored to stderr.
@@ -1064,27 +1044,35 @@ when "web"
1064
1044
  options[:web_markdown] = true
1065
1045
  elsif arg == "--no-web-markdown"
1066
1046
  options[:web_markdown] = false
1067
- elsif arg == "--web-turn-view"
1068
- options[:web_turn_view] = true
1069
- elsif arg == "--no-web-turn-view"
1070
- options[:web_turn_view] = false
1047
+ elsif arg == "--web-view" && Samagotchi::Config::BY_KEY["web.view"].enum_values.include?(web_args[i + 1])
1048
+ options[:web_view] = web_args[i + 1]
1049
+ elsif arg.start_with?("--web-view=") && Samagotchi::Config::BY_KEY["web.view"].enum_values.include?(arg.split("=", 2).last)
1050
+ options[:web_view] = arg.split("=", 2).last
1071
1051
  elsif arg.start_with?("--port=")
1072
1052
  options[:web_port] = arg.split("=", 2).last.to_i
1073
1053
  end
1074
1054
  end
1075
1055
  end
1076
1056
 
1057
+ # The first interactive start of a new installed chi says what `chi update` would update.
1058
+ chi_update_hint = lambda do
1059
+ require "samagotchi/installed_gem"
1060
+ require "samagotchi/update_hint"
1061
+ if Samagotchi::UpdateHint.wanted?(prompt: options[:prompt], non_interactive: options[:non_interactive],
1062
+ tty: $stdin.tty? && $stdout.tty?, installed: Samagotchi::InstalledGem.spec)
1063
+ Samagotchi::UpdateHint.show
1064
+ end
1065
+ end
1066
+
1077
1067
  if options[:web]
1068
+ chi_update_hint.call
1078
1069
  require "samagotchi/web/server"
1079
- port = options[:web_port] || Samagotchi::Config.get("web.port") || 4567
1080
- # legacy env fallback
1081
- port = ENV.fetch("SAMAGOTCHI_WEB_PORT", port.to_s).to_i rescue port.to_i
1070
+ port = options[:web_port] || Samagotchi::Config.get("web.port")
1082
1071
  markdown = options[:web_markdown]
1083
1072
  markdown = Samagotchi::Config.get("web.markdown") if markdown.nil?
1084
- turn_view = options[:web_turn_view]
1085
- turn_view = Samagotchi::Config.get("web.turn_view") if turn_view.nil?
1073
+ view = options[:web_view] || Samagotchi::Config.get("web.view")
1086
1074
  exit Samagotchi::Web::Server.launch(port: port, scope: options[:scope] || "project", open_browser: options[:web_open],
1087
- markdown: markdown, turn_view: turn_view,
1075
+ markdown: markdown, view: view, new_token: options[:new_token],
1088
1076
  annotate_presets: Samagotchi::Config.get("web.annotate_presets"))
1089
1077
  end
1090
1078
 
@@ -1143,6 +1131,7 @@ if options[:memories] || options[:muted]
1143
1131
  end
1144
1132
  end
1145
1133
 
1134
+ chi_update_hint.call
1146
1135
  require "samagotchi/launch_mode"
1147
1136
  launch, launch_note = if scratch
1148
1137
  [:repl, nil]
@@ -1182,7 +1171,7 @@ if launch == :attached
1182
1171
  end
1183
1172
 
1184
1173
  begin
1185
- Samagotchi::TerminalUI.new(
1174
+ ended = Samagotchi::TerminalUI.new(
1186
1175
  prompt: options[:prompt],
1187
1176
  session_id: options[:resume],
1188
1177
  no_interrupt: options[:no_interrupt] || options[:non_interactive],
@@ -1193,6 +1182,8 @@ begin
1193
1182
  model_name: options[:model],
1194
1183
  scratch: scratch
1195
1184
  ).run
1185
+ # `chi -p X </dev/null`: the turn failed (nothing was sent again).
1186
+ exit 1 if ended == :turn_failed
1196
1187
  rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel => e
1197
1188
  warn "Error: #{e.message}"
1198
1189
  exit 1