samagotchi 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -1
- data/docs/cli.md +4 -4
- data/docs/hooks.md +1 -1
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bundle_command.rb +4 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +4 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +17 -4
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/command_steps.rb +35 -15
- data/lib/samagotchi/engine.rb +4 -3
- data/lib/samagotchi/guardrails/shell_lex.rb +79 -5
- data/lib/samagotchi/host_registry.rb +4 -0
- data/lib/samagotchi/kernel_loop.rb +27 -7
- data/lib/samagotchi/llm/chat_loop.rb +12 -6
- data/lib/samagotchi/memory_bundle/installer.rb +14 -0
- data/lib/samagotchi/model_list_store.rb +117 -0
- data/lib/samagotchi/model_profile.rb +43 -0
- data/lib/samagotchi/send_command.rb +5 -3
- data/lib/samagotchi/session.rb +5 -0
- data/lib/samagotchi/session_commands.rb +6 -2
- data/lib/samagotchi/session_manager.rb +9 -2
- data/lib/samagotchi/session_metrics.rb +6 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -4
- data/lib/samagotchi/terminal_ui.rb +35 -5
- data/lib/samagotchi/tool_activity.rb +38 -8
- data/lib/samagotchi/tools/builtins.rb +10 -7
- data/lib/samagotchi/tools/delegate.rb +14 -10
- data/lib/samagotchi/tools/execute.rb +3 -1
- data/lib/samagotchi/tools/task_runtime.rb +21 -0
- data/lib/samagotchi/turn_note.rb +10 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -1
- data/lib/samagotchi/web/public/app.js +23 -11
- data/lib/samagotchi/web/public/command_view.js +19 -5
- data/lib/samagotchi/web/public/index.html +51 -25
- data/lib/samagotchi/web/public/sessions_list.js +21 -4
- data/lib/samagotchi/web/public/stage_model.js +23 -2
- data/lib/samagotchi/web/public/stage_view.js +16 -11
- data/lib/samagotchi/web/public/task_stop.js +11 -2
- data/lib/samagotchi/web/public/timing.js +12 -7
- data/lib/samagotchi/web/public/turn_events.js +3 -1
- data/lib/samagotchi/web/public/turn_model.js +29 -15
- data/lib/samagotchi/web/public/turn_view.js +70 -11
- data/lib/samagotchi/worker.rb +24 -7
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7af50a3a7e90170a9d6b4f3f5ab5914a0ab536ea121b498779109dabfb7c0b51
|
|
4
|
+
data.tar.gz: 4ddba29c753f1a6900956cc5abbbef70c3d912286e252c4c9adf08534a773368
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 497f4731e7a4ca9c1bd5073bea8e897506d37a5fcdfd15ea394f5837edfc85d76fbaade739ec4f6de998f7da5a982c3a8a9c1d4674b1e218a50ab14fe673291b
|
|
7
|
+
data.tar.gz: 0c6428021607e8540dcb21801510eca0295be3fba2de0e09eb4c2fd3c2c362326301065b52c5bc7745e006768522312d1934659da4cacc93254560402c767e76
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,87 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.30.0] - 2026-10-05
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- chi saves the model ids each host last listed (`~/.local/state/samagotchi/model_lists.json`), so a command that
|
|
16
|
+
starts a session without listing the hosts itself knows what they serve.
|
|
17
|
+
- `chi send --new --model` and the `delegate` tool's `model:` warn about a model id the named host's saved list
|
|
18
|
+
doesn't have: `host 'box' doesn't list model 'gemma-smal' (did you mean: gemma-small?); started it anyway`, on
|
|
19
|
+
stderr / in the tool's result. The session still starts: some hosts serve ids they don't list (a one-model
|
|
20
|
+
llama.cpp server takes any name, OpenRouter's `:nitro`). A name that names no host, a host with no saved list, and
|
|
21
|
+
a list over a week old are not checked.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Web: an open step whose label is its narration's first line or its first call's title shows only its call count,
|
|
26
|
+
so it doesn't repeat the words its body starts with; closed, the label is unchanged.
|
|
27
|
+
- Web stage: an answered turn says its step count once (in the cloud's chip) and its time once (in the status row).
|
|
28
|
+
- Web stage: `execute` and the task tools are blue in the running-tool row and the ticks, as in the tool rows under
|
|
29
|
+
them; purple is thinking's colour only (other tools' ticks are a quieter blue-grey).
|
|
30
|
+
- Web stage: `edit` and `write` are blue in the running-tool row and the ticks too, as in the tool rows; green is a
|
|
31
|
+
finished call's `✓` and an answered turn's only.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- A command the user's Stop killed (or kept from starting) is saved as `stopped`, not `error`: the web shows it with
|
|
36
|
+
■ and `stopped`, and the turn's `(N failed)` and the session's tool errors no longer count it.
|
|
37
|
+
- Web: the stop-task confirm names a task from an earlier turn by its command (the wait row's title), not its id.
|
|
38
|
+
- Web: a canceled turn's end line (the Stop, Ctrl-C, a hook, the session's stop) reads `■ canceled (stopped)` in the
|
|
39
|
+
stopped calls' amber, live and after a reload, not as a red error; a worker that exited mid-turn stays red `✕`.
|
|
40
|
+
- Web stage: a call the Stop cut has an amber tick and an amber `■` in the trail, as its `■ STOPPED` row, instead of
|
|
41
|
+
its tool kind's colour.
|
|
42
|
+
- A call still running when its turn is canceled is saved in the session's tool records as `stopped` (was
|
|
43
|
+
`canceled`), the word its row uses; one cut by a failed turn stays `canceled`.
|
|
44
|
+
- A canceled turn's last step keeps the thinking it streamed, so reloading it shows the thought instead of an empty
|
|
45
|
+
step (both loops).
|
|
46
|
+
- A plain REPL (`--no-shared`) keeps its session's `pending_card.json` as a worker does, so `chi web` marks a session
|
|
47
|
+
waiting on an open card it showed.
|
|
48
|
+
- source-links: a ref that names its own repo (`other/repo#12`) takes `{host}` from the git remote only when the
|
|
49
|
+
source's `remote_host:` lists that host, so it can't link to the local checkout's host.
|
|
50
|
+
- Web stage: the running-tool row no longer reads `thinking…` once the turn has ended (answered, canceled or
|
|
51
|
+
failed): it is idle, with no word.
|
|
52
|
+
- The REPL and attached mode end a canceled turn with an amber `■` (`■ turn canceled (Ctrl-C)`, `■ turn stopped by
|
|
53
|
+
loop-guard`), as the web does, instead of `✕`: a stop someone chose, not an error; a failed turn keeps `✕`.
|
|
54
|
+
- Web stage: a call a guardrail blocked has a red tick and a red `✕` in the trail, as its `✕ BLOCKED` row, instead
|
|
55
|
+
of its tool kind's colour.
|
|
56
|
+
- A command step with more than one heredoc (`-f body="$(cat <<EOF …)" -f title="$(cat <<EOF …)"`) cuts every body
|
|
57
|
+
out of its text, and its chip says so: `EOF · 3 lines +1`, with all of them on hover.
|
|
58
|
+
- A session worker survives a failed save between turns too (its first prompt, a command, taken notes, a dropped
|
|
59
|
+
question): it logs it and keeps going; notes whose save failed are kept and saved on the next pass.
|
|
60
|
+
|
|
61
|
+
Update with `chi update` (source-links 0.3.4, mcp 0.4.4) and restart `chi web`.
|
|
62
|
+
|
|
63
|
+
## [0.29.0] - 2026-10-05
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- Web: a session card shows `looped` when loop-guard stopped its last turn (`stopped by <name>` for another hook),
|
|
68
|
+
like `chi sessions list`'s `[looped]`; the all-sessions search finds them by those words.
|
|
69
|
+
- `chi bundle install` warns when a bundle needs a newer chi and its guardrail rules won't load (it already warned
|
|
70
|
+
about its hooks and plugin).
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- Web: a tool row shows its status as a mark (✓, ✕, ■, a spinner while it runs) instead of the word, so its command,
|
|
75
|
+
output and diff start right under the tool name; error, stopped and blocked calls keep their word after the title.
|
|
76
|
+
An open step's body sits indented under its summary.
|
|
77
|
+
- Web: a step with a failed call says so in its collapsed label, `3 tool calls (1 failed)`, with a red chevron; the
|
|
78
|
+
turn's summary counts failed calls below 3 calls too.
|
|
79
|
+
- Web: a `task_wait`, `task_get` or `task_stop` row names its task by the task's command instead of its id, a wait with
|
|
80
|
+
how long it waits (`bundle exec rspec · up to 600s`), also for a task started in an earlier turn; the id line stays
|
|
81
|
+
on hover.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- Guardrails: `> /tmp/x cat <<EOF` (a redirection written with a space before the command) is read as a heredoc of
|
|
86
|
+
data like `cat > /tmp/x <<EOF`, and a `)` inside quotes or a heredoc body no longer ends a `$(…)` early.
|
|
87
|
+
- `!cmd` no longer hands a `--model` worker's model to a chi it runs as the default model.
|
|
88
|
+
- A session worker survives a failed save at the end of a turn (it logs it) instead of crashing.
|
|
89
|
+
- A turn that ends with an empty answer removes only its own retry note, not an earlier turn's cancel or failure note.
|
|
90
|
+
- Web: streamed chunks from llama.cpp no longer carry its whole echoed prompt to every web client.
|
|
91
|
+
|
|
11
92
|
## [0.28.0] - 2026-10-04
|
|
12
93
|
|
|
13
94
|
### Added
|
|
@@ -1471,7 +1552,9 @@ and long-lived sessions.
|
|
|
1471
1552
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
1472
1553
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
1473
1554
|
|
|
1474
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
1555
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.30.0...HEAD
|
|
1556
|
+
[0.30.0]: https://github.com/dm1try/samagotchi/compare/v0.29.0...v0.30.0
|
|
1557
|
+
[0.29.0]: https://github.com/dm1try/samagotchi/compare/v0.28.0...v0.29.0
|
|
1475
1558
|
[0.28.0]: https://github.com/dm1try/samagotchi/compare/v0.27.0...v0.28.0
|
|
1476
1559
|
[0.27.0]: https://github.com/dm1try/samagotchi/compare/v0.26.0...v0.27.0
|
|
1477
1560
|
[0.26.0]: https://github.com/dm1try/samagotchi/compare/v0.25.0...v0.26.0
|
data/docs/cli.md
CHANGED
|
@@ -849,12 +849,12 @@ While a turn runs, you can cancel it without exiting the process:
|
|
|
849
849
|
Behavior notes:
|
|
850
850
|
|
|
851
851
|
- Cancellation returns control to the prompt immediately; what you typed there stays.
|
|
852
|
-
- The turn ends with one line,
|
|
852
|
+
- The turn ends with one line, `■ turn canceled (Ctrl-C) · 3.1s` (`■`, a stop someone chose, in amber), and for a prompt turn a dim
|
|
853
853
|
`partial progress kept; !rollback restores the pre-turn state` under it (a canceled continue is back where it
|
|
854
854
|
started). A failed turn ends with `✕ turn failed: <summary> · 2.0s` and a dim `prompt restored for retry`. The
|
|
855
|
-
REPL and attached mode say the same; the web says
|
|
856
|
-
`by a hook` for a hook's). A turn a bundle's plugin stopped names it:
|
|
857
|
-
|
|
855
|
+
REPL and attached mode say the same; the web says `■ canceled (Ctrl-C)` (`stopped` for its Stop button,
|
|
856
|
+
`by a hook` for a hook's). A turn a bundle's plugin stopped names it: `■ turn stopped by loop-guard` (the web:
|
|
857
|
+
`■ stopped by loop-guard`).
|
|
858
858
|
- Visible text the canceled request had streamed stays in the conversation, marked `[interrupted]`, so the next
|
|
859
859
|
message (or a continue) picks up from the half-finished reply; the canceled request's thinking and any unfinished
|
|
860
860
|
tool call are dropped.
|
data/docs/hooks.md
CHANGED
|
@@ -179,7 +179,7 @@ What a cut is, in detail:
|
|
|
179
179
|
- It is an empty answer made early: it uses the `retry.empty_answer` budget
|
|
180
180
|
([configuration.md](configuration.md#llama-network-retry-behavior)). With
|
|
181
181
|
no retry left (`retry.empty_answer: 0`, or already used) and nothing
|
|
182
|
-
queued, the turn ends cancelled (hook): "
|
|
182
|
+
queued, the turn ends cancelled (hook): "■ turn canceled (by a hook)".
|
|
183
183
|
- It shows nothing by itself: post your own notice (`event[:notify]`) to say
|
|
184
184
|
why. The bundle and the reason go into the note the model reads and the
|
|
185
185
|
log (`generation_stopped`).
|
data/docs/plugins.md
CHANGED
|
@@ -863,7 +863,7 @@ What happens (`action: retry`, the default):
|
|
|
863
863
|
`loop-guard> thinking repeats itself ("Wait, the count of the letter r…", 3 sentences ×3, 4k chars, 8 s): cut`
|
|
864
864
|
(a short run or a window says how many different sentences it held:
|
|
865
865
|
"12 different sentences in 48"), and `↻ cut by loop-guard, asking again (1/1)`.
|
|
866
|
-
2. If the retry loops too, the turn is stopped ("stopped the turn: …", "
|
|
866
|
+
2. If the retry loops too, the turn is stopped ("stopped the turn: …", "■
|
|
867
867
|
turn stopped by loop-guard") with a card that quotes the repeated
|
|
868
868
|
sentences.
|
|
869
869
|
3. A loop is forgotten after `forget_after` good steps in a row (generations
|
data/docs/sessions.md
CHANGED
|
@@ -56,7 +56,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
56
56
|
|
|
57
57
|
`--dry-run` is the safe preview. `--keep N` keeps the N newest sessions that stay: one that goes anyway (a leftover scratch session, one left empty, one older than `--days`) takes no place in the count. Web has no prune endpoint; use the CLI.
|
|
58
58
|
|
|
59
|
-
`chi sessions list` shows each session's saved recap, its first sentence without the "The user was…" opening (as on the web cards), in place of its last prompt, cut to 60 characters; a session with no recap shows its last prompt. A session whose last turn a hook stopped carries `[looped]` when the hook was loop-guard (the thinking watch's cut, or its stop) and `[stopped by <hook>]` for any other hook, after `[archived]` (`--format json` has it as `stopped_by`; from `Session#last_turn`, so it stays until a later turn ends differently). A delegated session's row ends with `↳ <parent's short id>` (plain and `--live`; `--format json` has `parent_id`); rows keep their `updated_at` order, so a parent may be on another page than its children. After the status, `ctx 12%` says how full the context was after the session's last counted turn (from its `analytics.json`; blank when no turn had a server count or the window is unknown; `ctx_pct` in `--format json`). The web's session cards show it quietly too (`12%`), and a session's ctx meter shows it on load, before its next turn streams.
|
|
59
|
+
`chi sessions list` shows each session's saved recap, its first sentence without the "The user was…" opening (as on the web cards), in place of its last prompt, cut to 60 characters; a session with no recap shows its last prompt. A session whose last turn a hook stopped carries `[looped]` when the hook was loop-guard (the thinking watch's cut, or its stop) and `[stopped by <hook>]` for any other hook, after `[archived]` (`--format json` has it as `stopped_by`; from `Session#last_turn`, so it stays until a later turn ends differently). The web's session cards wear the same marker as a badge: `looped` or `stopped by <hook>` after the status dot (a session that also waits on you shows only its waiting badge: the stop is behind the need), and the all-view's search matches its words too. A delegated session's row ends with `↳ <parent's short id>` (plain and `--live`; `--format json` has `parent_id`); rows keep their `updated_at` order, so a parent may be on another page than its children. After the status, `ctx 12%` says how full the context was after the session's last counted turn (from its `analytics.json`; blank when no turn had a server count or the window is unknown; `ctx_pct` in `--format json`). The web's session cards show it quietly too (`12%`), and a session's ctx meter shows it on load, before its next turn streams.
|
|
60
60
|
|
|
61
61
|
**Projects:** a session belongs to the git project it was started in: the repository, whichever worktree or subfolder of it (the same project root memories use). `chi sessions list` (with `--live` and `--format` too) and `chi web` show the current folder's project; `--scope=all` shows every session, and so does a folder in no repo (`~`). `--cwd PATH` is a folder filter instead of the project. The project is stored with the session (`project_root` in its JSON, `project` in `--format json`), so a session keeps it after its worktree is deleted; sessions older than that are placed by their folder, and one whose folder is gone shows in `--scope=all` only. Retention, `--resume`/`--attach ID`, `chi send`/`chi note ID` and `chi note --all` (every live session) are not scoped. The agent's `list_sessions` lists its own project's sessions; `cwd: "/"` lists every one.
|
|
62
62
|
|
|
@@ -168,7 +168,7 @@ chi send --wait -m "and the tests?" 3fa2 # a follow-up in the same
|
|
|
168
168
|
chi send --wait 3fa2 # sends nothing: waits for its next reply
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
- `--new` takes no ids (one new session per call). `--dir DIR` is its folder, and so its project (default the current one); `--model M` its model (default the configured one
|
|
171
|
+
- `--new` takes no ids (one new session per call). `--dir DIR` is its folder, and so its project (default the current one); `--model M` its model (default the configured one). A host config.yml doesn't have is refused before anything is spawned (exit 1). A `host:id` whose id that host's last saved list doesn't have (`~/.local/state/samagotchi/model_lists.json`, what `chi models` / `/models` / the web last listed for that host, at most a week old) gets one warning line on stderr, with up to three close ids as a hint (`did you mean: gemma-small?`), and the session starts anyway: some hosts serve ids they don't list (a one-model llama.cpp server takes any name). A bare id (no `host:`) isn't checked that way — routing sends it to whichever host lists it — and with no saved list for the host nothing is said; a wrong id then fails the worker's first turn.
|
|
172
172
|
- `--new` prints `<full id> started` on stdout. With `--wait` that line (and the `sent` line for an existing session) goes to stderr, so stdout is exactly the answer: the reply of the turn the message started, in full. It is the worker's `output/` file, the same one `delegate` reads; a turn that ends with only tool calls and no text has none.
|
|
173
173
|
- `--wait` takes one session: `--new` or one id. A session with a running turn is refused (`busy: a turn is running; wait or attach`, exit 1): the message would run after it, and its reply would come back as the answer.
|
|
174
174
|
- Exit codes with `--wait`: 0 answered; 3 the turn waits for an answer from you (a question, a hook's question, a guardrail approval, or the step-limit question: see [chi as a sub-agent](sub-agent.md#the-step-limit)), and the session keeps waiting: stderr gets the whole question (`waiting for an answer (approval): …`, its text, the numbered options, whether free text is allowed) and how to answer it (`chi answer ID --question QID --option N`, or `chi --attach ID`, the web); 4 `--timeout S` passed with the turn still running (it goes on; wait again); 1 the turn ended without a reply (canceled, failed or empty), the worker failed or vanished, or the session was stopped; 130 on Ctrl-C, which leaves the turn running (`still running: chi --attach ID`). There is no default timeout.
|
|
@@ -179,7 +179,7 @@ chi send --wait 3fa2 # sends nothing: waits fo
|
|
|
179
179
|
|
|
180
180
|
## Delegating
|
|
181
181
|
|
|
182
|
-
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is;
|
|
182
|
+
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail like an `execute` result. Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (a `↳` chip on its card, `delegated by` in its info bar, listed right after its parent in the all-sessions view) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
|
|
183
183
|
|
|
184
184
|
- `delegate(task, model:, session:, wait:, timeout:)`: starts a child and, by default, waits for its reply (`wait: false` returns at once, so several children can run side by side; `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting). With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one.
|
|
185
185
|
- `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one.
|
|
@@ -36,7 +36,8 @@ module Samagotchi
|
|
|
36
36
|
build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
|
|
37
37
|
Build local memories and installed hooks (and plugin) into a shareable bundle (dir or zip).
|
|
38
38
|
--scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
|
|
39
|
-
FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones)
|
|
39
|
+
FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones);
|
|
40
|
+
a named memory brings its model overlays (<name>.<model-key>.md).
|
|
40
41
|
|
|
41
42
|
trash [--empty] [--dry-run] [--older-than DAYS]
|
|
42
43
|
List bundle trash (moved files from uninstalls/upgrades).
|
|
@@ -61,7 +62,8 @@ module Samagotchi
|
|
|
61
62
|
build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
|
|
62
63
|
Build local memories and installed hooks into a shareable bundle (dir or zip).
|
|
63
64
|
--scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
|
|
64
|
-
FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones)
|
|
65
|
+
FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones);
|
|
66
|
+
a named memory brings its model overlays (<name>.<model-key>.md).
|
|
65
67
|
|
|
66
68
|
Examples:
|
|
67
69
|
chi bundle build --scope system
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.4.
|
|
3
|
+
version: 0.4.4
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session, its tools are mcp_<server>_<tool>; /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files: {}
|
|
8
8
|
plugin:
|
|
9
9
|
file: plugin.rb
|
|
10
|
-
sha256: sha256:
|
|
10
|
+
sha256: sha256:05543038e2950a5391655184f57a1028133eda8ca05f9e46e5cd2eddc3e9e608
|
|
11
11
|
requires_chi: ">= 0.8.0"
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
# attach_image_paths: true # default: an image path in the text, in the
|
|
19
19
|
# # temp dir or cwd, attaches it
|
|
20
20
|
#
|
|
21
|
-
# Image blocks in a result go to the model as images (text
|
|
22
|
-
# image/png
|
|
21
|
+
# Image blocks in a result go to the model as images (the text keeps
|
|
22
|
+
# "[image 1: image/png]"); so does an image file named in a text block,
|
|
23
23
|
# under the system temp dir or the server's cwd: the block is only its
|
|
24
24
|
# absolute path (chrome-devtools-mcp --slim answers a screenshot that way),
|
|
25
|
-
# or it says one ending in an image extension ("Saved it to /tmp/a.png.")
|
|
25
|
+
# or it says one ending in an image extension ("Saved it to /tmp/a.png."),
|
|
26
|
+
# noted as "[image 1: a.png]".
|
|
26
27
|
# A server that doesn't start, answer or list its tools is skipped with a
|
|
27
28
|
# notice; the rest of chi works. One that exits mid-session starts again on
|
|
28
29
|
# its next call, at most MAX_RESTARTS times a session. /mcp lists the
|
|
@@ -40,8 +40,12 @@ require "open3"
|
|
|
40
40
|
# A `url:` template takes {match} (group 1, else the whole ref), {1}…{9}
|
|
41
41
|
# (numbered groups), {name} (named groups), and {repo}/{host}: the named
|
|
42
42
|
# group when it took part, else the project's (Dir.pwd's) git remote, asked
|
|
43
|
-
# of git once per worker. A
|
|
44
|
-
#
|
|
43
|
+
# of git once per worker. A {repo} the ref itself names ("other/repo#12")
|
|
44
|
+
# is that ref's own, so its {host} comes from the remote only when the
|
|
45
|
+
# source's `remote_host:` list has the remote's host: else the ref would
|
|
46
|
+
# link to this checkout's host with someone else's repo. A ref with a
|
|
47
|
+
# placeholder that can't be filled is not linked; a `{word}` that is none
|
|
48
|
+
# of these stays text (warned at load).
|
|
45
49
|
#
|
|
46
50
|
# The note is not stored in the conversation: it is an event, replayed by a
|
|
47
51
|
# UI only while the session's worker lives (a reload keeps it; a stopped
|
|
@@ -457,7 +461,7 @@ class SourceLinks
|
|
|
457
461
|
when /\A\d+\z/ then match[word.to_i]&.then { |value| escape_url(value) }
|
|
458
462
|
when "repo", "host"
|
|
459
463
|
value = match.names.include?(word) ? match[word] : nil
|
|
460
|
-
value ||= remote_value(word, source)
|
|
464
|
+
value ||= remote_value(word, source, repo_from_match: match.names.include?("repo") ? match["repo"] : nil)
|
|
461
465
|
word == "repo" ? escape_repo(value) : value&.then { |host| escape_url(host) }
|
|
462
466
|
else match[word]&.then { |value| escape_url(value) }
|
|
463
467
|
end
|
|
@@ -466,7 +470,16 @@ class SourceLinks
|
|
|
466
470
|
# {repo} / {host} from the project's git remote (the source's `remote:`,
|
|
467
471
|
# default origin); nil when there is none, or when its host is not one of
|
|
468
472
|
# the source's `remote_host:` list.
|
|
469
|
-
|
|
473
|
+
#
|
|
474
|
+
# +repo_from_match+: the match's own {repo}, when it has one. Such a ref
|
|
475
|
+
# names its own repo (`other/repo#12`), not the local checkout's, so its
|
|
476
|
+
# {host} may only come from the remote when the source lists that remote's
|
|
477
|
+
# host (remote_host:): else the ref would link to the local remote's host
|
|
478
|
+
# with someone else's repo.
|
|
479
|
+
def remote_value(word, source, repo_from_match: nil)
|
|
480
|
+
derived_host = word == "host" && !repo_from_match.to_s.empty?
|
|
481
|
+
return nil if derived_host && source[:remote_hosts].empty?
|
|
482
|
+
|
|
470
483
|
remote = project_remote(source[:remote])
|
|
471
484
|
return nil unless remote
|
|
472
485
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: source-links
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.4
|
|
4
4
|
scope: system
|
|
5
5
|
description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, and a one-line note after the turn (note false turns it off); sources are configured patterns
|
|
6
6
|
trust_level: reviewed
|
|
@@ -8,7 +8,7 @@ files:
|
|
|
8
8
|
source_links.md: sha256:aebd37f1e25491780994ecc4afc20ed6e8db32b724dc038302bb050177efada8
|
|
9
9
|
hooks:
|
|
10
10
|
source_links.rb:
|
|
11
|
-
sha256: sha256:
|
|
11
|
+
sha256: sha256:c93269964ebc824b5f15ab60db81a458facd50de61446c550f254ea4c68964e2
|
|
12
12
|
event: after_turn
|
|
13
13
|
on_error: log
|
|
14
14
|
priority: 90
|
|
@@ -9,7 +9,9 @@ module Samagotchi
|
|
|
9
9
|
# command as the model wrote it. Rules, in order:
|
|
10
10
|
#
|
|
11
11
|
# - a heredoc's body leaves its step's text and becomes its +heredoc+
|
|
12
|
-
# ({tag:, lines:}), also one inside a "$(cat <<'EOF' … EOF)" argument;
|
|
12
|
+
# ({tag:, lines:}), also one inside a "$(cat <<'EOF' … EOF)" argument; a
|
|
13
|
+
# step with more than one is cut of them all, and lists them as
|
|
14
|
+
# +heredocs+ (+heredoc+ stays the first; the web's chip names it, +N);
|
|
13
15
|
# - steps are split at && || ; | |& & and newlines; a ( … ) subshell stays
|
|
14
16
|
# one step; +op+ is the operator before a step (nil for the first);
|
|
15
17
|
# - a leading "cd X &&" / "cd X;" becomes +cd+ (only the first);
|
|
@@ -23,8 +25,8 @@ module Samagotchi
|
|
|
23
25
|
# parentheses) gives nil: the UI then shows the title and the raw
|
|
24
26
|
# command.
|
|
25
27
|
module CommandSteps
|
|
26
|
-
Step = Data.define(:text, :op, :label, :limit, :heredoc) do
|
|
27
|
-
def initialize(text:, op: nil, label: nil, limit: nil, heredoc: nil) = super
|
|
28
|
+
Step = Data.define(:text, :op, :label, :limit, :heredoc, :heredocs) do
|
|
29
|
+
def initialize(text:, op: nil, label: nil, limit: nil, heredoc: nil, heredocs: nil) = super
|
|
28
30
|
|
|
29
31
|
def to_h = super.compact
|
|
30
32
|
end
|
|
@@ -122,29 +124,47 @@ module Samagotchi
|
|
|
122
124
|
|
|
123
125
|
slice = text[from...to]
|
|
124
126
|
heredoc = body && { tag: bodies[body.last], lines: body_lines(text[body.last]) }
|
|
125
|
-
slice,
|
|
126
|
-
Step.new(text: slice.strip, op: op, heredoc: heredoc
|
|
127
|
+
slice, heredocs = inner_heredocs(slice) if heredoc.nil?
|
|
128
|
+
Step.new(text: slice.strip, op: op, heredoc: heredoc || heredocs&.first,
|
|
129
|
+
heredocs: heredocs && heredocs.size > 1 ? heredocs : nil)
|
|
127
130
|
end
|
|
128
131
|
|
|
129
|
-
#
|
|
130
|
-
# the slice with
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
# Every heredoc inside a $(…) in +slice+ (git commit -m "$(cat <<'EOF'
|
|
133
|
+
# …)"): the slice with all their bodies cut out, and the heredocs ([] for
|
|
134
|
+
# none). One step can hold several ("-f body=$(cat <<EOF …)" and
|
|
135
|
+
# "-f title=$(cat <<EOF …)"), and the rest of the step is text the reader
|
|
136
|
+
# would otherwise see.
|
|
137
|
+
def inner_heredocs(slice)
|
|
138
|
+
heredocs = []
|
|
139
|
+
return [slice, heredocs] unless slice.include?("<<")
|
|
133
140
|
|
|
134
141
|
at = 0
|
|
135
142
|
while (open = slice.index("$(", at))
|
|
136
143
|
lexer = Lex::Lexer.new(slice[(open + 2)..])
|
|
137
144
|
lexer.tokens
|
|
138
145
|
doc = lexer.heredoc_spans.first
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
to += 1 if slice[to] == "\n"
|
|
143
|
-
return [slice[0...from] + slice[to..], { tag: doc[:tag], lines: body_lines(slice[(from + 1)...to]) }]
|
|
146
|
+
unless doc
|
|
147
|
+
at = open + 2
|
|
148
|
+
next
|
|
144
149
|
end
|
|
150
|
+
|
|
151
|
+
from = open + 2 + doc[:span].begin - 1 # the newline before the body
|
|
152
|
+
to = open + 2 + doc[:span].end
|
|
153
|
+
to += 1 if slice[to] == "\n"
|
|
154
|
+
heredocs << { tag: doc[:tag], lines: body_lines(slice[(from + 1)...to]) }
|
|
155
|
+
cut = slice[0...from] + slice[to..]
|
|
156
|
+
# A span that cut nothing (it can't) would loop here: move past it.
|
|
157
|
+
if cut.length >= slice.length
|
|
158
|
+
at = open + 2
|
|
159
|
+
next
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
slice = cut
|
|
163
|
+
# The cut moved everything after it left; keep looking from the same
|
|
164
|
+
# spot, past this "$(".
|
|
145
165
|
at = open + 2
|
|
146
166
|
end
|
|
147
|
-
[slice,
|
|
167
|
+
[slice, heredocs]
|
|
148
168
|
end
|
|
149
169
|
|
|
150
170
|
# The body's line count, its terminator line not counted.
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -1942,11 +1942,12 @@ module Samagotchi
|
|
|
1942
1942
|
# that way: the note, and nothing made up as its answer. The note's
|
|
1943
1943
|
# marker is for the UIs (the notice, and the empty steps after a
|
|
1944
1944
|
# reload); it goes on the result's conversation too (the REPL keeps it).
|
|
1945
|
-
# A retry's nudge at the tail goes: this note says it all.
|
|
1945
|
+
# A retry's nudge at the tail goes: this note says it all. An earlier
|
|
1946
|
+
# turn's note stays: it says why that turn ended.
|
|
1946
1947
|
if result.empty_answer?
|
|
1947
1948
|
note = TurnNote.empty(retries: result.empty_retries, steps: result.empty_steps)
|
|
1948
|
-
conversation&.replace(TurnNote.
|
|
1949
|
-
saved = TurnNote.
|
|
1949
|
+
conversation&.replace(TurnNote.without_trailing_nudge(conversation))
|
|
1950
|
+
saved = TurnNote.without_trailing_nudge(conversation || turn.session.messages)
|
|
1950
1951
|
conversation << note if conversation
|
|
1951
1952
|
saved + [note]
|
|
1952
1953
|
elsif result.canceled? && conversation
|
|
@@ -169,19 +169,76 @@ module Samagotchi
|
|
|
169
169
|
add(SUBST, close + 1 - @i)
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
+
# From $( to the ) that closes it, as one SUBST. A quote or a
|
|
173
|
+
# heredoc body inside it is skipped over: a ) in one of those
|
|
174
|
+
# doesn't close the substitution.
|
|
172
175
|
def substitution
|
|
173
176
|
depth = 0
|
|
174
177
|
j = @i + 1
|
|
175
178
|
loop do
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
break if j >= @s.size
|
|
180
|
+
|
|
181
|
+
c = @s[j]
|
|
182
|
+
if ["'", '"'].include?(c)
|
|
183
|
+
j = quote_end(j)
|
|
184
|
+
elsif c == "("
|
|
185
|
+
depth += 1
|
|
186
|
+
j += 1
|
|
187
|
+
elsif c == ")"
|
|
188
|
+
depth -= 1
|
|
189
|
+
j += 1
|
|
190
|
+
break if depth.zero?
|
|
191
|
+
elsif @s[j, 2] == "<<"
|
|
192
|
+
j = substitution_heredoc(j)
|
|
193
|
+
else
|
|
194
|
+
j += 1
|
|
195
|
+
end
|
|
180
196
|
end
|
|
181
197
|
@unterminated = true unless depth.zero?
|
|
182
198
|
add(SUBST, j - @i)
|
|
183
199
|
end
|
|
184
200
|
|
|
201
|
+
# The index after the quote starting at +j+, a backslash in a double
|
|
202
|
+
# quote escaping the next character; the text's end when it is never
|
|
203
|
+
# closed.
|
|
204
|
+
def quote_end(j)
|
|
205
|
+
quote = @s[j]
|
|
206
|
+
k = j + 1
|
|
207
|
+
while k < @s.size
|
|
208
|
+
if quote == '"' && @s[k] == "\\" then k += 2
|
|
209
|
+
elsif @s[k] == quote then return k + 1
|
|
210
|
+
else k += 1
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
k
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# From a << at +j+ inside a $(…) past its body's terminator line
|
|
217
|
+
# (nothing is lexed: the body is only stepped over), or on to the
|
|
218
|
+
# next character when no tag follows or the terminator never comes
|
|
219
|
+
# (a << that is no heredoc, as arithmetic's 1<<3, stays scanned).
|
|
220
|
+
def substitution_heredoc(j)
|
|
221
|
+
dash = @s[j + 2] == "-"
|
|
222
|
+
k = j + (dash ? 3 : 2)
|
|
223
|
+
k += 1 while [" ", "\t"].include?(@s[k])
|
|
224
|
+
tag, _quoted, stop = heredoc_tag(k)
|
|
225
|
+
return j + 1 if tag.empty?
|
|
226
|
+
|
|
227
|
+
eol = @s.index("\n", stop)
|
|
228
|
+
return j + 1 if eol.nil?
|
|
229
|
+
|
|
230
|
+
k = eol + 1
|
|
231
|
+
while k < @s.size
|
|
232
|
+
eol = @s.index("\n", k) || @s.size
|
|
233
|
+
line = @s[k...eol]
|
|
234
|
+
line = line.sub(/\A\t+/, "") if dash
|
|
235
|
+
return [eol + 1, @s.size].min if line == tag
|
|
236
|
+
|
|
237
|
+
k = eol + 1
|
|
238
|
+
end
|
|
239
|
+
j + 1
|
|
240
|
+
end
|
|
241
|
+
|
|
185
242
|
def comment
|
|
186
243
|
@i = @s.index("\n", @i) || @s.size
|
|
187
244
|
end
|
|
@@ -306,10 +363,27 @@ module Samagotchi
|
|
|
306
363
|
def data_sink?(at)
|
|
307
364
|
start = at
|
|
308
365
|
start -= 1 while start.positive? && @tokens[start - 1][0] == :word
|
|
309
|
-
verb = @tokens[start...at]
|
|
366
|
+
verb = command_word(@tokens[start...at])
|
|
310
367
|
next_op = @tokens[at..].find { |t| t[0] == :op }
|
|
311
368
|
DATA_SINKS.include?(File.basename(verb.to_s)) && !["|", "|&"].include?(next_op&.dig(1))
|
|
312
369
|
end
|
|
370
|
+
|
|
371
|
+
# The command word among the words before a heredoc's: the NAME=
|
|
372
|
+
# words and the redirections dropped, and with a redirection written
|
|
373
|
+
# with a space (> /tmp/x) its target word too.
|
|
374
|
+
def command_word(words)
|
|
375
|
+
i = 0
|
|
376
|
+
while i < words.size
|
|
377
|
+
word = words[i][1]
|
|
378
|
+
if word.match?(REDIRECT)
|
|
379
|
+
i += 1 if word.match?(/\A\d*[<>]{1,2}\z/)
|
|
380
|
+
elsif !word.match?(/\A[A-Za-z_]\w*=/)
|
|
381
|
+
return word
|
|
382
|
+
end
|
|
383
|
+
i += 1
|
|
384
|
+
end
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
313
387
|
end
|
|
314
388
|
end
|
|
315
389
|
end
|
|
@@ -5,6 +5,7 @@ require_relative "config"
|
|
|
5
5
|
require_relative "log"
|
|
6
6
|
require_relative "model_ref"
|
|
7
7
|
require_relative "client"
|
|
8
|
+
require_relative "model_list_store"
|
|
8
9
|
require_relative "llm/openai_chat"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
@@ -287,6 +288,9 @@ module Samagotchi
|
|
|
287
288
|
models = list_models_for(entry)
|
|
288
289
|
data = { host: entry.host, port: entry.port, transport: entry.transport, models: models, error: nil }
|
|
289
290
|
@mutex.synchronize { @host_lists[name] = { data: data, at: @clock.call } }
|
|
291
|
+
# On disk too: a process that spawns a worker without listing
|
|
292
|
+
# (`chi send --new --model`, delegate) checks an id against it.
|
|
293
|
+
ModelListStore.save(name, models.map(&:id))
|
|
290
294
|
rescue StandardError => e
|
|
291
295
|
Log.warn(:model, "list_failed", host: name, error: e.class.name, msg: e.message.to_s[0, 500])
|
|
292
296
|
data = { host: entry.host, port: entry.port, transport: entry.transport, models: [], error: e.message }
|