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/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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ };