nomoremeatproxy 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +122 -3
- package/bin/nomeat.mjs +365 -0
- package/docs/assets/not-a-meat-proxy.png +0 -0
- package/docs/usage.md +466 -0
- package/package.json +59 -6
- package/src/adapters/claude-attended.mjs +51 -0
- package/src/adapters/claude.mjs +158 -0
- package/src/adapters/codex-app.mjs +58 -0
- package/src/adapters/codex-cli.mjs +38 -0
- package/src/adapters/codex-daemon.mjs +47 -0
- package/src/adapters/codex-transport.mjs +90 -0
- package/src/adapters/codex.mjs +268 -0
- package/src/adapters/opener.mjs +29 -0
- package/src/call.mjs +193 -0
- package/src/check.mjs +46 -0
- package/src/doctor-live.mjs +123 -0
- package/src/doctor.mjs +213 -0
- package/src/history/claude.mjs +263 -0
- package/src/history/codex.mjs +164 -0
- package/src/history/compact.mjs +149 -0
- package/src/history/folder.mjs +10 -0
- package/src/history/index.mjs +35 -0
- package/src/history/match.mjs +20 -0
- package/src/history/render.mjs +23 -0
- package/src/ledger.mjs +118 -0
- package/src/listen.mjs +36 -0
- package/src/listener-lease.mjs +88 -0
- package/src/mailbox.mjs +145 -0
- package/src/orient.mjs +72 -0
- package/src/peers.mjs +18 -0
- package/src/remote-control-store.mjs +116 -0
- package/src/remote-control.mjs +64 -0
- package/src/standing.mjs +54 -0
- package/src/stats.mjs +65 -0
- package/src/usage.mjs +44 -0
- package/src/writable.mjs +49 -0
- package/templates/peer-calls.md +136 -0
- package/tools/doctor.mjs +109 -0
package/docs/usage.md
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# Using `nomeat`
|
|
2
|
+
|
|
3
|
+
One command that lets an assistant on one side call an assistant on the other, and look up
|
|
4
|
+
chats on the other side.
|
|
5
|
+
|
|
6
|
+
Examples below use a source checkout. With the npm package installed and on your PATH,
|
|
7
|
+
replace `node bin/nomeat.mjs` with `nomeat`. Run `npm run doctor` from the installed package
|
|
8
|
+
directory or source checkout; it is not a separate command added to your PATH.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Node.js 22.15 or later.
|
|
13
|
+
- Codex CLI and Claude Code CLI installed and signed in with your subscriptions.
|
|
14
|
+
- Windows or macOS for listing sessions in the Claude app; the calls themselves do not need
|
|
15
|
+
it. On macOS the opener runs under `script`, which gives it the terminal it needs. On
|
|
16
|
+
Linux the vendor offers no opener, so a bridge conversation shows in Claude Desktop only
|
|
17
|
+
when a chat there is listening (`listen --once`, see below); this was confirmed on Linux.
|
|
18
|
+
- On macOS, keep the project and the state folder outside Documents, Desktop and Downloads.
|
|
19
|
+
The first time the Codex app touches a project in one of them, the system asks for access to
|
|
20
|
+
that folder, and no installer can grant it ahead.
|
|
21
|
+
|
|
22
|
+
Run `npm run doctor` to check. It reports what is installed and whether both command lines
|
|
23
|
+
are signed in with the subscription, and calls nothing. A sign-in through an API key or a
|
|
24
|
+
third party shows as a `warning`: calls would use paid API credentials. A model can need a newer command line than the bridge does:
|
|
25
|
+
`claude-fable-5-2` (the peer `fable`) is refused with "API Error 400" before Claude CLI
|
|
26
|
+
2.1.290, so an older version shows as a `warning` that names that model. The row
|
|
27
|
+
`state-folder` says whether the state folder (see [State](#state)) exists or can be created,
|
|
28
|
+
and can be written; when it cannot, the row is `missing` and names `NOMEAT_STATE_DIR`.
|
|
29
|
+
`doctor` exits 1 when a required row is `missing`, and 0 otherwise. The desktop apps,
|
|
30
|
+
browsers and WSL are checked on Windows only; elsewhere those rows are `unknown` and say that
|
|
31
|
+
they are not checked on this platform. The host line names the distribution and its version
|
|
32
|
+
where `/etc/os-release` exists, as on Linux.
|
|
33
|
+
|
|
34
|
+
`npm run doctor -- --live` also makes one real, read-only, one-word call to `astra` and one to
|
|
35
|
+
`fable` through `nomeat call`, each in a new thread, from the caller `doctor-check`. For each
|
|
36
|
+
side it reports whether the command line was found, whether the call answered (which shows
|
|
37
|
+
that sign-in and the model work) and what `app.opened` said (Claude only). It exits 1 when a
|
|
38
|
+
side fails, including a Claude chat whose `app.opened` is `false`. A side is called only when its
|
|
39
|
+
command line and its subscription sign-in are both `found`, once, with a request id of its own
|
|
40
|
+
and no retry. After 11 minutes its call and every process it started are ended, and the result
|
|
41
|
+
is uncertain; if ending them does not succeed, the detail says so. The details use fixed wording, never the vendor's output. The live part is
|
|
42
|
+
printed, never written with `--write`.
|
|
43
|
+
|
|
44
|
+
## Calling a peer
|
|
45
|
+
|
|
46
|
+
### First use: orient without sending
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node bin/nomeat.mjs orient --from astra --cwd <target-project>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`--cwd` is the project whose routes you want, **not necessarily the bridge installation**.
|
|
53
|
+
Without it, the current directory is resolved to an absolute lexical path; symlinks are not
|
|
54
|
+
canonicalized. The report shows the state location, an advisory filesystem access check,
|
|
55
|
+
standing/fallback destinations, incoming and outgoing routes involving `--from`, and Claude
|
|
56
|
+
listener registrations. Custom caller names remain valid; known peers are shown first, at
|
|
57
|
+
most 20 routes, with `routesOmitted` for any remainder. With no `--from`, existing project
|
|
58
|
+
routes are shown. Peer names identify configured roles, not proof of the model running.
|
|
59
|
+
|
|
60
|
+
Orientation creates no folders, write probes, daemons or messages. An access check can pass
|
|
61
|
+
while a sandbox still refuses a write. Listener registration includes fresh heartbeats and
|
|
62
|
+
held `listen --once` claims; it does not prove that the chat will receive or answer a call.
|
|
63
|
+
Codex callers do not run `listen`; a receiving Claude Desktop chat does, in that exact chat.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node bin/nomeat.mjs call --to astra --from fable --effort ultra --message "Review the plan in docs/plan.md"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Long or multi-line messages go in on stdin:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
node bin/nomeat.mjs call --to fable --from astra < question.md
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Peer | Side | Default model | Default thinking level |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `astra` | Codex | `gpt-6-astra` | `ultra` |
|
|
78
|
+
| `sol` | Codex | `gpt-6.1-sol` | `ultra` |
|
|
79
|
+
| `fable` | Claude | `claude-fable-5-2` | none (the vendor's own) |
|
|
80
|
+
| `opus` | Claude | `claude-opus-5-5` | `high` |
|
|
81
|
+
|
|
82
|
+
The defaults apply when a call starts a new thread. A message into a Codex thread that already
|
|
83
|
+
exists, the standing one or one named with `--thread`, keeps that thread's own model and level;
|
|
84
|
+
only `--model` or `--effort`, when given, replace them. A Claude session is the one difference:
|
|
85
|
+
a continued session is still given the peer's model and level, since it has no stored setting
|
|
86
|
+
the command line would fall back to.
|
|
87
|
+
|
|
88
|
+
- `--from` names the caller and is required. The peer sees it in a one-line header, so it can
|
|
89
|
+
tell a peer's message from your own. The header is plain text, not proof of who sent it.
|
|
90
|
+
- `--effort` replaces the peer's default thinking level and is passed through as given. Codex
|
|
91
|
+
accepts `low` to `ultra`; Claude accepts `low` to `max`. The result says which level was
|
|
92
|
+
applied (`effort`); a queued or attended delivery applies none, and then the result lists
|
|
93
|
+
`effort` under `notApplied` if you gave `--effort`.
|
|
94
|
+
- `--model` overrides the peer's model. A queued or attended delivery runs with the chat's own
|
|
95
|
+
model, and the result lists `model` under `notApplied` if you gave `--model`.
|
|
96
|
+
- `--write` lets the peer edit files in the project. The default is read-only: a Codex peer
|
|
97
|
+
runs in a read-only sandbox, and a Claude peer gets only its reading tools (Read, Grep,
|
|
98
|
+
Glob), which also makes each call much cheaper. A Claude peer that has to run a command or
|
|
99
|
+
search the web needs `--write`. The result says which access the call ran with (`access`).
|
|
100
|
+
A `--write` call that would go through the Codex app's queue is refused unless the bridge
|
|
101
|
+
created that thread with write access (see below).
|
|
102
|
+
|
|
103
|
+
- `--no-wait` hands the message over and returns at once with `"state": "sent"`. Use it when
|
|
104
|
+
the peer's work will take long, or when the peer is going to call you back: a caller that is
|
|
105
|
+
still waiting cannot be reached. It works for Codex peers, into a thread the Codex app has
|
|
106
|
+
open, and needs `--thread` or an earlier call to that peer. `--new --no-wait` is refused by
|
|
107
|
+
design, before anything is sent: call once with waiting to create the chat, then send with
|
|
108
|
+
`--thread <id> --no-wait`. Where the Codex command line
|
|
109
|
+
offers `codex queue --resume`, the bridge uses it to ask Codex to load the chat and run the
|
|
110
|
+
message; if that fails the message stays queued and the result carries Codex's `warning`
|
|
111
|
+
(only lines that mention resuming).
|
|
112
|
+
|
|
113
|
+
A queued message runs only when something has the chat loaded: the Codex app, for a chat it
|
|
114
|
+
is running, or Codex's own background daemon. So where it uses `--resume`, the bridge first
|
|
115
|
+
checks that the daemon runs and starts it if not (`codex app-server daemon start`, with a
|
|
116
|
+
time limit). The daemon keeps running afterwards; `codex app-server daemon stop` stops it.
|
|
117
|
+
Where Codex's daemon package is not installed yet, that first start makes Codex install it
|
|
118
|
+
without asking: about 370 MB on a Mac. The bridge does this by itself, by the owner's choice.
|
|
119
|
+
If it cannot be started, the message is still queued once, and the `warning` says it may
|
|
120
|
+
not run until the chat is opened in the Codex app.
|
|
121
|
+
|
|
122
|
+
### Which thread a call lands in
|
|
123
|
+
|
|
124
|
+
- If a Claude chat is **listening** for that peer name in this project (see below), the call
|
|
125
|
+
goes to that chat, and the chat itself answers.
|
|
126
|
+
- Otherwise a call **continues the standing thread** between the same caller and peer in the
|
|
127
|
+
same project folder. The first call starts it.
|
|
128
|
+
- `--new` starts a side thread. Later calls still continue the standing one.
|
|
129
|
+
- `--thread <id>` sends into that exact thread, including one you started yourself in the
|
|
130
|
+
app. The standing thread stays as it is.
|
|
131
|
+
- Neither of those two becomes the standing thread by itself, not even as the first call of
|
|
132
|
+
a route. The next ordinary call continues the standing thread, or starts it.
|
|
133
|
+
- `--standing`, added to a call, makes that call's thread the standing one from now on.
|
|
134
|
+
- `nomeat standing --to <peer> --from <caller>` shows the standing thread of a route, and
|
|
135
|
+
with `--thread <id>` names one without sending anything.
|
|
136
|
+
- A binding change reports the old and new named/fallback destinations, exactly which calls
|
|
137
|
+
are affected, a restoration command and argv, and `routing configured; delivery not tested`.
|
|
138
|
+
It affects only default calls for that exact project/caller/peer when a Claude listener does
|
|
139
|
+
not take precedence. Explicit `--thread`, `--new`, other routes and chat settings stay as
|
|
140
|
+
they were. Restoration uses `--thread <previous>` or `--clear` if the route was unnamed.
|
|
141
|
+
It pins the exact state location in `restore.environment.NOMEAT_STATE_DIR`; apply that
|
|
142
|
+
environment together with executable/argv. The display command carries it automatically
|
|
143
|
+
and restores the prior shell environment afterward on Windows.
|
|
144
|
+
`--clear` appends an event restoring unnamed fallback behavior; no history is deleted and
|
|
145
|
+
repeated clears leave the same routing behavior. Restoration is a new change, not a rollback
|
|
146
|
+
of intervening calls or settings. The display command targets PowerShell on Windows and a
|
|
147
|
+
POSIX shell elsewhere; executable/args plus environment need no shell interpretation.
|
|
148
|
+
If clearing leaves neither a standing nor an answered fallback thread, `stateDir` and
|
|
149
|
+
`stateNote` retain the state-folder guidance alongside the routing `note`. A read-only
|
|
150
|
+
`standing` lookup that finds nothing keeps that guidance in its existing `note` field.
|
|
151
|
+
- A Claude chat that answers from its own app, because it is listening or has answered a
|
|
152
|
+
call that way, cannot be the standing thread. The standing thread is continued without
|
|
153
|
+
the app, and that would start a second copy of a live chat. `nomeat standing` fails for
|
|
154
|
+
such a chat, and a call with `--thread <that chat> --standing` is refused before anything
|
|
155
|
+
is sent.
|
|
156
|
+
- A route used before standing threads existed keeps continuing its last answered thread,
|
|
157
|
+
and that thread becomes the standing one at the next ordinary call. Calls made with
|
|
158
|
+
`--new` or `--thread` after the upgrade do not count for this. Name the right one first
|
|
159
|
+
if it is not.
|
|
160
|
+
- New threads get a title such as `astra to fable [shop]: Review the plan`, where `shop` is
|
|
161
|
+
the last folder of the project path. New Claude sessions are
|
|
162
|
+
listed in the Claude app automatically on Windows and macOS (`--no-open` skips that); on
|
|
163
|
+
Linux the vendor offers no way to do that. A new Codex thread works at once and
|
|
164
|
+
`nomeat find --side codex` lists it. Once its turn has ended, the bridge opens it by its
|
|
165
|
+
link, `codex://threads/<id>`, so that the running Codex app lists it (`--no-open` skips
|
|
166
|
+
that too): with `cmd.exe /c start "" <link>` on Windows, `open <link>` on macOS and
|
|
167
|
+
`xdg-open <link>` on Linux. Opening the link switches the Codex app's main area to the new
|
|
168
|
+
chat, seen on Linux: if you are in another chat there, you are moved out of it. Whether the
|
|
169
|
+
window is raised or takes keyboard focus was not checked. Only a thread the call created is
|
|
170
|
+
opened, never one it continued; a call sent with `--no-wait` or through the app's queue
|
|
171
|
+
never creates one. The call returns, and the link is opened, only once the bridge's own
|
|
172
|
+
Codex server has ended, since while it runs it holds the thread; it waits up to 5 seconds,
|
|
173
|
+
and if the server is still running then, the link is opened anyway and `warning` and
|
|
174
|
+
`app.reason` say so.
|
|
175
|
+
|
|
176
|
+
What was seen. Windows: the bridge's opener ran twice, before it waited for its server, and
|
|
177
|
+
both chats showed in the app on their own, the second during an update of the app; a third run, with the wait for the bridge's own server in place, showed its chat too. Opened
|
|
178
|
+
by hand, the link made the app list a chat twice, once after a delay that was not timed and
|
|
179
|
+
once within seconds; with the app closed, it started the app, which came up on that new
|
|
180
|
+
chat, seen once. macOS and Linux: only the same commands run by hand, on older code without
|
|
181
|
+
the opener, once each. `open` exited 0 and the chat was listed in under 3 seconds by the
|
|
182
|
+
owner's estimate; `xdg-open` exited 0 in 0.02 seconds and one second later the chat was
|
|
183
|
+
open, first in Recents. Not tested: the link while the app is minimized, or closed on macOS
|
|
184
|
+
and Linux; for a chat whose turn is still running; from a shell without a desktop session,
|
|
185
|
+
or from inside Codex's sandbox. Where it does not work, the app shows the chat later: see
|
|
186
|
+
the known limits in the [README](../README.md#known-limits).
|
|
187
|
+
|
|
188
|
+
### A thread that is open in the Codex app
|
|
189
|
+
|
|
190
|
+
Codex allows one writer per thread. When the app holds the thread, the message goes through
|
|
191
|
+
Codex's queue and the app runs the turn itself. The result then says `"delivery": "queued"`,
|
|
192
|
+
and the thread's own model and settings apply instead of `--model` and `--effort`.
|
|
193
|
+
|
|
194
|
+
`--write` cannot raise a queued thread's access. Such a call is refused before anything is
|
|
195
|
+
queued, unless the bridge created the thread with `--write`. Send without `--write` if the
|
|
196
|
+
chat already has write access in the app, or start a thread with `--new --write`.
|
|
197
|
+
|
|
198
|
+
### Calls into the Claude chat you are sitting in
|
|
199
|
+
|
|
200
|
+
A Claude chat that is running in the desktop app cannot safely be resumed from outside: that
|
|
201
|
+
would start a second copy of it. A chat can listen for calls instead:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
node bin/nomeat.mjs listen --as fable --thread <this chat's session id>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The Claude assistant runs this as a background monitor in its own chat. While it runs:
|
|
208
|
+
|
|
209
|
+
- A call to `fable` in this project that names no thread arrives in that chat as one JSON
|
|
210
|
+
line, with `requestId`, `from`, `hop`, `maxHops`, `file`, `instruction` and `message`. The
|
|
211
|
+
`instruction` names the sender and the request id, and tells the chat to quote the whole
|
|
212
|
+
message and its sender in the chat before anything else, reading `file` first if the
|
|
213
|
+
message is cut off, then to write its answer and send that same answer back with
|
|
214
|
+
`node bin/nomeat.mjs reply --request-id <id> --message "..."`. Every request line carries
|
|
215
|
+
it, so this does not depend on the chat remembering the rule.
|
|
216
|
+
- The Claude app shows that line as a background notice, not as a chat bubble. The owner
|
|
217
|
+
sees the peer's words only because the chat prints them. A long notice is cut short at the
|
|
218
|
+
end, so `message` comes last and `instruction` before it; `file` holds the whole request.
|
|
219
|
+
In the other direction nothing changes: a message into a Codex chat arrives as a message of
|
|
220
|
+
that thread, and the Codex app shows it as one.
|
|
221
|
+
- The caller waits for that answer and gets `"delivery": "attended"`. `--model`, `--effort`
|
|
222
|
+
and `--write` do not apply: the chat answers as it is.
|
|
223
|
+
- A request retried under the same id is not printed again. Once its answer is there, the
|
|
224
|
+
retry gets it. While the chat holds it unanswered, a resend with `--force` is refused at
|
|
225
|
+
once, whether a chat is listening or not, with a note saying so; if it is still needed,
|
|
226
|
+
send it as a new call, without `--request-id`.
|
|
227
|
+
- `--thread` and `--new` still go where they say, without involving the listening chat.
|
|
228
|
+
- If the listening chat is itself waiting for an answer from the caller, the call runs
|
|
229
|
+
headless in the standing chat instead, because the listening chat could not answer.
|
|
230
|
+
- When nobody is listening (the monitor ended or the app closed), calls go back to the
|
|
231
|
+
standing chat within about 20 seconds.
|
|
232
|
+
|
|
233
|
+
The app limits a monitor to 30 minutes, so listening has to be started again to continue.
|
|
234
|
+
|
|
235
|
+
Optionally, add `--once` (it needs `--thread`) and run the listener as a plain background
|
|
236
|
+
command instead of a monitor. It works the same, but exits with code 0 after the first round
|
|
237
|
+
in which it printed something: one request (or several that arrived together), or a Remote
|
|
238
|
+
Control notice. The chat is notified when the command ends, so it pays a turn only when
|
|
239
|
+
something arrived, not every 30 minutes.
|
|
240
|
+
|
|
241
|
+
- When the command ends, start it again first, then answer. Calls that arrive in between wait
|
|
242
|
+
in the inbox and the next start delivers them.
|
|
243
|
+
- After a `--once` exit the chat still counts as listening for 5 minutes, and only a start
|
|
244
|
+
with the same `--thread` can take over. Started later than that, calls in the gap go to the
|
|
245
|
+
standing chat, as when nobody is listening. A chat that stops after a `--once` exit keeps
|
|
246
|
+
callers waiting for those 5 minutes before they fall back.
|
|
247
|
+
|
|
248
|
+
Only one chat can listen for a peer name in a project. A second chat is refused with exit
|
|
249
|
+
code 1 and a message naming the chat that is listening. The same chat, identified by
|
|
250
|
+
`--thread`, can restart at once; a different chat can take over about 20 seconds after the
|
|
251
|
+
first stops. Listening uses a database module built into Node, which prints a one-line
|
|
252
|
+
"experimental" warning on stderr.
|
|
253
|
+
|
|
254
|
+
### Following up on a message sent without waiting
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
node bin/nomeat.mjs check --request-id <id>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Says whether the peer has not started on the message (`waiting`), is working on it
|
|
261
|
+
(`running`) or has finished (`answered`, with its reply). Nothing is sent again. The reply is
|
|
262
|
+
the turn's final answer, as for a call that waits; the status lines Codex marks as interim are
|
|
263
|
+
left out.
|
|
264
|
+
|
|
265
|
+
A `waiting` message says only what can be seen. Under a minute after sending, the note says how
|
|
266
|
+
many seconds ago it was sent and that the peer has not started on it yet. After that, with
|
|
267
|
+
`"idle": true` the chat's newest turn is finished, so nothing is running there and the message has not been
|
|
268
|
+
picked up: open the chat in the Codex app, or send it again as a new call that waits (the
|
|
269
|
+
queued copy may then run as well). Otherwise the cause is unknown. Without
|
|
270
|
+
`--request-id` it checks every message of this project sent with `--no-wait` in the last 24
|
|
271
|
+
hours. Whoever sends without waiting is the one who follows up.
|
|
272
|
+
|
|
273
|
+
`check` reports the turn that the request started. A later turn in the same chat, such as the
|
|
274
|
+
owner's answer to a question card, is not part of it; `read` shows it.
|
|
275
|
+
|
|
276
|
+
Open observation, from a Mac: two messages sent with `--no-wait` into a thread the Codex app
|
|
277
|
+
held stayed `waiting` until that thread was loaded in the app the next day. The cause was not
|
|
278
|
+
examined.
|
|
279
|
+
|
|
280
|
+
### Reaching a bridge chat from another device
|
|
281
|
+
|
|
282
|
+
Newly imported Claude chats start with Remote Control **off**. To enable them automatically,
|
|
283
|
+
once per installation, start a named, attended Claude Desktop listener and opt in:
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
node bin/nomeat.mjs remote-control enable --as fable --thread <coordinator-id> --cwd <coordinator-project>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
From then on, a new chat opened by `nomeat call` is queued, including chats in other folders.
|
|
290
|
+
The primary reply does not wait. The bound Desktop chat gets a notice, uses its native
|
|
291
|
+
`set_remote_control` tool, and checks `get_session` for **on and active** before reporting
|
|
292
|
+
success. A target gets at most three notices, five minutes apart; if unanswered, it becomes
|
|
293
|
+
`unavailable` rather than waking the coordinator indefinitely. Only the new session ID
|
|
294
|
+
crosses projects, not its path or conversation. An offline
|
|
295
|
+
coordinator leaves durable pending work; restart its listener with the same project, peer,
|
|
296
|
+
and chat ID to pick it up. Restart any listener running pre-feature code once after upgrading.
|
|
297
|
+
|
|
298
|
+
Use `remote-control status --target <chat-id>` to see pending, blocked, or last verified
|
|
299
|
+
active (not a live connection check). If platform approval is required, approve in Desktop
|
|
300
|
+
and use `remote-control retry --target <chat-id>`. An opening failure, inactive host, or a
|
|
301
|
+
chat not yet visible in Desktop is not a successful verification.
|
|
302
|
+
|
|
303
|
+
Use `call --no-remote-control` to skip one new chat or `remote-control disable` to turn off
|
|
304
|
+
the default and cancel pending work. Neither changes already-enabled chats or revokes a native
|
|
305
|
+
Desktop action already in flight. Existing or
|
|
306
|
+
resumed chats, attended conversations, and `--no-open` calls are not enrolled. This enables
|
|
307
|
+
the Desktop session; it does **not** make later headless turns sync to a phone.
|
|
308
|
+
|
|
309
|
+
## Looking up a chat on the other side
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
node bin/nomeat.mjs find --side codex --search "Vercel"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
node bin/nomeat.mjs read --side codex --thread <id> --turns 2
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
node bin/nomeat.mjs find --side claude --text "token expired"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
- `--search` matches **titles** (a thread without a title is matched on the first line of
|
|
324
|
+
its first message). The result states what was searched in `matchedOn` and `scope`, and
|
|
325
|
+
`more` says whether further matches exist beyond `--limit`.
|
|
326
|
+
- `--text` searches inside messages: a chat matches when one message of the person or the
|
|
327
|
+
assistant contains all the words, in any order, ignoring case. Tool output, commands and
|
|
328
|
+
attachments are not searched. Each result adds `match`: who said it (`by`: `user` or
|
|
329
|
+
`assistant`), when (`at`, if the store has it) and a one-line `excerpt` around the match.
|
|
330
|
+
Only the most recent chats are looked through: 200 session files on the Claude side, 20
|
|
331
|
+
threads on the Codex side, where listing the threads is slow. With `--project` on the Codex
|
|
332
|
+
side, up to 20 of the project's threads are read from the newest the app lists (at most
|
|
333
|
+
100). `scope` says how many were listed and looked through, and whether older ones were
|
|
334
|
+
left out. With `--search` as well, both must match. An empty `--text` is a usage error.
|
|
335
|
+
- `--project` keeps only the chats whose folder is this project (or `--cwd`). Use it to find
|
|
336
|
+
a chat whose title does not mention the project, with or without `--search`. On the Codex
|
|
337
|
+
side it looks through the newest threads the app returns in one request (at most 100);
|
|
338
|
+
`scope` says how many. It goes by the folder the app recorded for the chat: a Codex chat started in the app outside any project sits in a
|
|
339
|
+
folder the app made for it and is not found this way, even if it works on the project.
|
|
340
|
+
`find --side codex --text <a word typed into it>` does find it. `nomeat standing` shows
|
|
341
|
+
which thread a route uses, whatever its folder.
|
|
342
|
+
- `read` now defaults to compact user/assistant text, newest 2 **visible conversation turns**
|
|
343
|
+
in chronological order, with turn indices, `startedAt` when available and nullable `status`.
|
|
344
|
+
The newest Codex turn in the saved thread snapshot reports an ambiguous `interrupted`
|
|
345
|
+
status as `interrupted, or still running`, just like the raw view; older interrupted turns
|
|
346
|
+
stay `interrupted`. Status is retained on every fragment, and Claude status is null.
|
|
347
|
+
Hidden-only notification or compaction turns do not crowd out recent conversation. `--tools` adds
|
|
348
|
+
described tool/command/file events and Claude tool results; private reasoning stays hidden.
|
|
349
|
+
Structured Claude compaction, meta/sidechain and task/peer notification records are excluded,
|
|
350
|
+
along with complete legacy `/compact`/compaction-output envelopes. Ordinary prose mentioning
|
|
351
|
+
those terms is not filtered. Incoming bridge task/peer notification envelopes are
|
|
352
|
+
intentionally counted and hidden: the listening assistant's visible request quotation,
|
|
353
|
+
required by each notification's show rule, stays readable. Use `--raw` to inspect the
|
|
354
|
+
original control notification. Unknown vendor formats may still appear.
|
|
355
|
+
- `--budget` bounds the **entire minified JSON plus its newline**, in UTF-8 bytes (default
|
|
356
|
+
16000, minimum 2048). If even metadata or one scalar cannot fit, reading fails rather than
|
|
357
|
+
exceeding the budget. `--max-chars` limits a text fragment to 4000 UTF-16 code units by
|
|
358
|
+
default; a supplementary Unicode scalar needs two units even with `--max-chars 1`.
|
|
359
|
+
- Feed `nextCursor` back as `--cursor` with the same side, thread and `--tools` view to finish
|
|
360
|
+
the current window, including inside a long message. Once `nextCursor` is null,
|
|
361
|
+
`olderCursor` navigates to the preceding visible-turn window. Each window is chronological;
|
|
362
|
+
windows are visited newest to oldest. Both null means there is no more visible material.
|
|
363
|
+
`--turns` is ignored for a cursor; budget and fragment size can change. Cursors are
|
|
364
|
+
thread/view-scoped snapshots: edits invalidate the selected window, while appended turns
|
|
365
|
+
are counted as `newerTurns` outside it. Restart without a cursor to see new material.
|
|
366
|
+
Cursor schema v2 preserves whole-thread status context across fragments and older windows;
|
|
367
|
+
restart reads made with pre-review v1 cursors rather than reusing them.
|
|
368
|
+
- `omitted` separately counts items/text units outside the window, filtered items by reason,
|
|
369
|
+
text before/after the current page and split items. A partially displayed item counts in
|
|
370
|
+
both its page and the corresponding before/after item count; text-unit counts do not
|
|
371
|
+
overlap. Text units mean UTF-16 units of displayable descriptions, not raw transcript bytes;
|
|
372
|
+
private/unsupported blocks are counted without inspecting or exposing their text.
|
|
373
|
+
- **Migration:** consumers of the former CLI result should add `--raw`. It preserves the old
|
|
374
|
+
tool-inclusive `turns` shape, raw-turn selection and per-item `--max-chars` clipping. It
|
|
375
|
+
cannot use compact `--budget`, `--cursor` or `--tools`. Exported legacy history readers,
|
|
376
|
+
content search and the independent call/check history consumers keep their old behavior.
|
|
377
|
+
- `--side codex` covers the threads the Codex app lists. `--side claude` covers the Claude
|
|
378
|
+
Code session files on this machine, and accepts ids with the app's `local_` prefix.
|
|
379
|
+
- Web chats on chatgpt.com and claude.ai are not covered.
|
|
380
|
+
|
|
381
|
+
## Result of a call
|
|
382
|
+
|
|
383
|
+
One JSON object on stdout:
|
|
384
|
+
|
|
385
|
+
| Field | Meaning |
|
|
386
|
+
|---|---|
|
|
387
|
+
| `state` | `answered`, `sent` (with `--no-wait`), `duplicate`, `uncertain` or `refused` |
|
|
388
|
+
| `threadId` | The thread on the peer's side |
|
|
389
|
+
| `continued` | Whether an existing thread was continued |
|
|
390
|
+
| `requestId` | Reuse it on a retry |
|
|
391
|
+
| `reply` | The peer's answer, when `answered`. From a Codex peer, the turn's final answer: the status lines Codex marks as interim are left out, and where it marks none, the last message is the answer |
|
|
392
|
+
| `delivery` | `direct` or `queued` for Codex peers; `headless` for a Claude run; `attended` when a listening Claude chat answered |
|
|
393
|
+
| `access` | `read-only` or `write`: what the call set, or else, for a Codex thread, the access the bridge created it with (a queued call, or a resumed one without `--write`). Absent when unknown |
|
|
394
|
+
| `effort` | The thinking level applied. Absent for queued and attended deliveries, or when none was set |
|
|
395
|
+
| `app` | For a new Claude chat: `opened` is `true` when the opener that lists it in the Claude app exited with code 0 within 20 seconds, and `false` with a `reason` otherwise (it did not start, exited with another code, or ran out of time; the reason then says whether a stop signal was sent or it could not be stopped). On macOS that exit code is the opener's own. On Windows it only shows that the opener's console window was launched, not that the chat was listed. Where the vendor offers no opener, as on Linux, nothing is attempted: `opened` is `false` and `supported` is `false`, a field present only in that case. For a new Codex chat: `opened` is `true` when the program that opens its link exited with code 0 within 10 seconds. That shows only that the system took the link, not that the app listed the chat. Otherwise `opened` is `false` with a `reason`, also for a thread id that is not a UUID, which is never put into a link. `supported` is `false` only where there is no opener: on a platform other than Windows, macOS and Linux, or when the opener program, such as `xdg-open`, is not installed. When the bridge's own Codex server had not ended within 5 seconds, the link is opened anyway and `reason` says so, ahead of the opener's own reason if there is one. A failure to open never fails the call |
|
|
396
|
+
| `notApplied` | `model` and/or `effort`, when you gave them on the command line and a queued or attended delivery ran the turn with the chat's own settings. The message was still delivered. Absent when nothing was dropped |
|
|
397
|
+
| `recovered` | Present when a retry found the answer already in the thread |
|
|
398
|
+
| `warning` | What Codex warned about a queued message, such as a chat it could not load. For a direct Codex call: that the bridge's own Codex server had not exited 5 seconds after it was told to stop, so it may still hold the thread; the answer stands. Absent when there was nothing |
|
|
399
|
+
| `note` | What to do, when nothing was sent |
|
|
400
|
+
|
|
401
|
+
Exit codes: `0` done, `1` failed, `2` usage error, `3` not sent.
|
|
402
|
+
|
|
403
|
+
## What calls cost
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
node bin/nomeat.mjs stats --since 2026-11-10T00:00:00Z
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Summarises the answered calls of this project by caller, peer and way of delivery: how many,
|
|
410
|
+
the median size of message and reply in characters, the median time to answer, and token use.
|
|
411
|
+
|
|
412
|
+
- Token figures exist only where the vendor reports them to the bridge: `direct` Codex calls
|
|
413
|
+
and `headless` Claude calls. They cover the peer that answered, not the caller.
|
|
414
|
+
- `total` is everything the model processed for the call, the fixed load of instructions and
|
|
415
|
+
tool definitions included. That load can dwarf the message; historical measurements are
|
|
416
|
+
source-only and are not included in the npm package.
|
|
417
|
+
|
|
418
|
+
## Retries and limits
|
|
419
|
+
|
|
420
|
+
- Retry with the same `--request-id`. An answered request is never sent again.
|
|
421
|
+
- After a failure that may have delivered the message, a retry first looks in the thread. If
|
|
422
|
+
the answer is there it is returned (`recovered`); otherwise the state is `uncertain`. Resend
|
|
423
|
+
with `--force` only after checking that the message is absent. Recovery works for Codex
|
|
424
|
+
peers and for a listening Claude chat that answered late; for a headless Claude run use
|
|
425
|
+
`read` to check.
|
|
426
|
+
- `--hop` counts positions in one exchange and `--max-hops` bounds it (default 4). Past the
|
|
427
|
+
limit the call is refused and the question goes to the owner.
|
|
428
|
+
- A Claude call that no listening chat takes runs the Claude command line, which needs the
|
|
429
|
+
network and a writable folder for its sessions (`projects` under `CLAUDE_CONFIG_DIR`, or
|
|
430
|
+
under `.claude` in your home folder). Before starting it the bridge checks both: it creates
|
|
431
|
+
and removes an empty folder there, and only if that works looks up the host the command
|
|
432
|
+
line talks to with the system's own resolver, in a short child process that is ended after
|
|
433
|
+
3 seconds. That costs one such process, about 0.1 seconds, per headless Claude call. The
|
|
434
|
+
lookup is skipped for an address or `localhost` in `ANTHROPIC_BASE_URL`, and when `HTTPS_PROXY`,
|
|
435
|
+
`HTTP_PROXY` or `ALL_PROXY` is set, because behind a proxy it can fail while the call works. When a check
|
|
436
|
+
fails, the call fails at once with exit code 1, nothing is sent, and a retry is not
|
|
437
|
+
`uncertain`. Inside Codex's sandbox on Linux and macOS both fail: run the call outside the
|
|
438
|
+
sandbox, or have a Claude chat listen (see above) so that the call is delivered to it.
|
|
439
|
+
|
|
440
|
+
## State
|
|
441
|
+
|
|
442
|
+
The state folder is `.nomoremeatproxy` in your home folder on Windows, or
|
|
443
|
+
`$XDG_STATE_HOME/nomoremeatproxy` elsewhere. `NOMEAT_STATE_DIR` overrides it. It must be one
|
|
444
|
+
folder that both assistants see. A side that uses another folder sees none of the other's
|
|
445
|
+
calls, without an error: when `standing` finds no thread, or `check` no such request, the result
|
|
446
|
+
names the folder it read in `stateDir` and says that both sides must use the same one. On Windows it is deliberately not under `AppData`: each
|
|
447
|
+
Store app gets a private copy of that folder, so the two desktop apps would each keep their
|
|
448
|
+
own state and calls from one side would never continue a chat.
|
|
449
|
+
|
|
450
|
+
When the state folder refuses a write for lack of permission, or does not exist and cannot be
|
|
451
|
+
created, the command fails with an error that names `NOMEAT_STATE_DIR`, not with the system's
|
|
452
|
+
own message. The same holds for a command that only reads the state, such as `standing`,
|
|
453
|
+
`check` or `stats`: a folder that does not exist and cannot be created is reported, not taken
|
|
454
|
+
for an empty first run. A folder that does not exist yet but can be created reads as empty,
|
|
455
|
+
and a read does not create it. A call writes its
|
|
456
|
+
first ledger entry before anything is sent: when that write is refused, nothing was sent, and
|
|
457
|
+
the error ends with "nothing was sent". A
|
|
458
|
+
write refused later, such as the one that records the answer, comes after the peer was
|
|
459
|
+
contacted, so the message may have been delivered and the work done; check the thread with
|
|
460
|
+
`read` before sending again. Inside Codex's sandbox the default folder cannot be written; there,
|
|
461
|
+
set `NOMEAT_STATE_DIR` to a folder the sandbox can write, and the same on the other side.
|
|
462
|
+
|
|
463
|
+
- `ledger.jsonl` holds ids, states, project folders, message hashes, the sizes of message and
|
|
464
|
+
reply and the vendor's token figures, never message text.
|
|
465
|
+
- `inbox/` is the mailbox for listening chats. It holds a message and its answer only until
|
|
466
|
+
the caller has collected the answer; leftovers are removed after a day.
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nomoremeatproxy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "nomoremeatproxy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Lets Codex and Claude sessions find each other's history and exchange work without a human carrying messages.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nomeat": "bin/nomeat.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.15"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/nomeat.mjs",
|
|
15
|
+
"src/adapters/claude-attended.mjs",
|
|
16
|
+
"src/adapters/claude.mjs",
|
|
17
|
+
"src/adapters/codex-app.mjs",
|
|
18
|
+
"src/adapters/codex-cli.mjs",
|
|
19
|
+
"src/adapters/codex-daemon.mjs",
|
|
20
|
+
"src/adapters/codex-transport.mjs",
|
|
21
|
+
"src/adapters/codex.mjs",
|
|
22
|
+
"src/adapters/opener.mjs",
|
|
23
|
+
"src/call.mjs",
|
|
24
|
+
"src/check.mjs",
|
|
25
|
+
"src/doctor-live.mjs",
|
|
26
|
+
"src/doctor.mjs",
|
|
27
|
+
"src/history/claude.mjs",
|
|
28
|
+
"src/history/codex.mjs",
|
|
29
|
+
"src/history/compact.mjs",
|
|
30
|
+
"src/history/folder.mjs",
|
|
31
|
+
"src/history/index.mjs",
|
|
32
|
+
"src/history/match.mjs",
|
|
33
|
+
"src/history/render.mjs",
|
|
34
|
+
"src/ledger.mjs",
|
|
35
|
+
"src/listen.mjs",
|
|
36
|
+
"src/listener-lease.mjs",
|
|
37
|
+
"src/mailbox.mjs",
|
|
38
|
+
"src/orient.mjs",
|
|
39
|
+
"src/peers.mjs",
|
|
40
|
+
"src/remote-control-store.mjs",
|
|
41
|
+
"src/remote-control.mjs",
|
|
42
|
+
"src/standing.mjs",
|
|
43
|
+
"src/stats.mjs",
|
|
44
|
+
"src/usage.mjs",
|
|
45
|
+
"src/writable.mjs",
|
|
46
|
+
"tools/doctor.mjs",
|
|
47
|
+
"docs/usage.md",
|
|
48
|
+
"docs/assets/not-a-meat-proxy.png",
|
|
49
|
+
"templates/peer-calls.md"
|
|
50
|
+
],
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"registry": "https://registry.npmjs.org/"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "node --test",
|
|
56
|
+
"doctor": "node tools/doctor.mjs",
|
|
57
|
+
"doctor:write": "node tools/doctor.mjs --write docs/environment.md"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Hands a call to a Claude chat that is listening in the desktop app, so that chat answers
|
|
2
|
+
// itself. The alternative, a headless run on the same session, would be a second writer.
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
4
|
+
const POLL_MS = 500;
|
|
5
|
+
|
|
6
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
const notSent = (message) => Object.assign(new Error(message), { notSent: true });
|
|
8
|
+
const HELD = 'the request was already handed to a listening chat and its answer has not come; send a new request if it is still needed';
|
|
9
|
+
|
|
10
|
+
export async function deliverToAttended({ mailbox, box, listener, request, timeoutMs = DEFAULT_TIMEOUT_MS, pollMs = POLL_MS, sleep = wait, now = () => Date.now() }) {
|
|
11
|
+
mailbox.post({ ...box, ...request });
|
|
12
|
+
const deadline = now() + timeoutMs;
|
|
13
|
+
while (now() < deadline) {
|
|
14
|
+
const reply = mailbox.takeReply(request.requestId);
|
|
15
|
+
if (reply !== null) return { threadId: listener.session, reply, delivery: 'attended', continued: true };
|
|
16
|
+
if (!mailbox.listener(box) && mailbox.withdraw(request.requestId)) {
|
|
17
|
+
throw notSent('the attended Claude chat stopped listening before it took the message');
|
|
18
|
+
}
|
|
19
|
+
await sleep(pollMs);
|
|
20
|
+
}
|
|
21
|
+
if (mailbox.withdraw(request.requestId)) throw notSent('the attended Claude chat did not take the message in time');
|
|
22
|
+
throw new Error(`the attended Claude chat took the message but did not answer within ${Math.round(timeoutMs / 1000)} s`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Sends to the listening chat when there is one and the caller did not name a destination;
|
|
26
|
+
// otherwise, or when that chat cannot take the message, runs headless.
|
|
27
|
+
export function routeClaude({ mailbox, headless, isWaitingOn, deliver = deliverToAttended }) {
|
|
28
|
+
return async (args) => {
|
|
29
|
+
if (args.noWait) throw notSent('sending without waiting is only available for Codex peers');
|
|
30
|
+
// A resend of a request a chat still holds is refused at once, listening or not: a chat
|
|
31
|
+
// that stopped listening may still be working on it, and a headless run would be a copy.
|
|
32
|
+
if (!args.pinned && mailbox.taken(args.requestId)) throw Object.assign(new Error(HELD), { refused: true });
|
|
33
|
+
const box = { project: args.cwd, peer: args.peer };
|
|
34
|
+
const listener = args.pinned ? null : mailbox.listener(box);
|
|
35
|
+
// A chat that is itself waiting for this caller's answer cannot reply; a headless run can.
|
|
36
|
+
if (!listener || isWaitingOn({ project: args.cwd, from: args.peer, peer: args.from })) return headless(args);
|
|
37
|
+
const request = { requestId: args.requestId, from: args.from, hop: args.hop, maxHops: args.maxHops, message: args.message };
|
|
38
|
+
try {
|
|
39
|
+
return await deliver({ mailbox, box, listener, request, timeoutMs: args.timeoutMs });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (!error.notSent) throw error;
|
|
42
|
+
return headless(args);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// For a retry: the answer may have arrived after the caller stopped waiting.
|
|
48
|
+
export const recoverAttendedReply = (mailbox) => async ({ requestId }) => {
|
|
49
|
+
const reply = mailbox.takeReply(requestId);
|
|
50
|
+
return reply === null ? null : { reply, delivery: 'attended' };
|
|
51
|
+
};
|