@webinson/peer 0.4.15 → 0.4.17

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Peer
2
2
 
3
3
  See which people on your team have agents running, on what they work, and which stopped lately.
4
- Only the Claude Code threads where someone typed `/peer` are shared.
4
+ Only the Claude Code and Codex threads explicitly shared through Peer appear in the list.
5
5
 
6
6
  ```bash
7
7
  npm install -g @webinson/peer
@@ -19,12 +19,12 @@ In a Claude Code thread:
19
19
  - `/peer stop` ends sharing. Quitting or restarting Claude Code only pauses it: the thread shows as stopped and
20
20
  is shared again when the session is resumed. `/clear` and signing out of Claude Code end it.
21
21
  - A band above the prompt shows the thread's memories (`17 memories`), `Use Elephant` and `Use Jellyfish`, and
22
- `● Active` (shared) or `○ Inactive` at the right; hovering a control explains it. The modes decide what memory
23
- the thread uses:
24
- - **Use Elephant** (shared): your OpenViking memory, recalled with each prompt and saved as the thread goes, on
25
- top of Claude Code's own memory. It saves only the turns from the moment it was chosen.
26
- - **Inactive** (not shared): Claude Code's own memory alone; OpenViking stays off.
27
- - **Use Jellyfish** (shared): no memory at all, neither OpenViking nor Claude Code's own (`MEMORY.md`).
22
+ `● Active` (shared) or `○ Inactive` at the right; a press says in a toast what the thread now uses. The modes
23
+ decide what memory the thread uses:
24
+ - **Use Elephant** (shared): PeerMemory (your personal OpenViking), recalled with each prompt and saved as the
25
+ thread goes, on top of Claude Code's own memory. It saves only the turns from the moment it was chosen.
26
+ - **Inactive** (not shared): Claude Code's own memory alone; PeerMemory stays off.
27
+ - **Use Jellyfish** (shared): no memory at all, neither PeerMemory nor Claude Code's own (`MEMORY.md`).
28
28
  `CLAUDE.md` instructions stay.
29
29
 
30
30
  Pressing the lit mode again stops sharing (Inactive); pressing either while Inactive shares again. Every thread
@@ -79,7 +79,56 @@ a terminal does.
79
79
 
80
80
  - A resumed or forked session can get a new session id. It is then not shared until `/peer` again.
81
81
  - Subagents do not appear: the hooks Peer uses fire for the main thread only.
82
- - Codex is not supported yet.
82
+
83
+ ## Codex
84
+
85
+ Install the npm package and set up the Peer terminal with:
86
+
87
+ ```bash
88
+ npm install -g @webinson/peer
89
+ peer login --codex --client you@company.com
90
+ peer-codex
91
+ ```
92
+
93
+ If you already signed in to Peer, use `peer install --codex --client` in place of the login
94
+ command. Review and trust the Peer Thread Mark hooks in Codex's `/hooks` menu.
95
+
96
+ The `--client` setup needs Git, Rust/Cargo, `curl`, `shasum`, and a C/C++ toolchain on
97
+ the machine. It builds the pinned Codex client under `~/.peer/codex-client`. If you already
98
+ built it, set `PEER_CODEX_CLIENT` to its executable when you run setup. The `peer-codex`
99
+ launcher uses your existing Codex settings and sign-in with its own app server. `PEER_HOME`
100
+ moves the default checkout and Peer state.
101
+
102
+ For the plugin alone, omit `--client`. The Codex plugin lives under [`mod/codex`](mod/codex)
103
+ in its own marketplace.
104
+
105
+ In stock Codex, send `peer: start`, `peer: stop`, `peer: list`, `peer: thread`, or
106
+ `peer: all [words] [here] [--newest | --oldest] [page N]`. The hook answers these locally.
107
+ This plugin uses the [Codex hook API](https://learn.chatgpt.com/docs/hooks); stock Codex does
108
+ not expose a composer band or memory switches to plugins.
109
+
110
+ From a source checkout, use `node bin/peer.mjs install --codex --client` and
111
+ `bash mod/codex/client/run.sh` from `cli/`.
112
+
113
+ The terminal band shows the thread's memory count, **Use Elephant**, **Use Jellyfish**, and
114
+ **Active** or **Inactive**. Press the lit mode again to stop sharing. Use `/peer-mode` for
115
+ keyboard controls. `/peer [message]` shares the thread and submits an optional message;
116
+ `/peer stop` stops sharing. `/peer-list`, `/peer-thread` and `/peer-all` open terminal panels.
117
+ The threads panel refreshes while open; the memory search accepts the same words and filters
118
+ as Claude Code.
119
+
120
+ - **Elephant** shares the thread and allows the installed OpenViking plugin and Codex's own
121
+ memory. OpenViking receives only the turns from this thread's Elephant intervals.
122
+ - **Jellyfish** shares the thread with both memory integrations off. Instructions such as
123
+ `AGENTS.md` stay available. Codex's own memory can be read again in another mode. Its native
124
+ generation remains off for that thread so another Codex client sharing the same history
125
+ cannot extract the Jellyfish turns later.
126
+ - **Inactive** stops sharing. Codex's own memory follows your usual settings; the OpenViking
127
+ plugin is off in this client.
128
+
129
+ The mode applies to future turns. Context already present in the chat stays there. Use the
130
+ Peer terminal client to enforce these modes; stock Codex and separately configured memory tools
131
+ follow their own settings. `peer uninstall --codex` removes the plugin from Codex.
83
132
 
84
133
  ## Team memory (optional)
85
134
 
@@ -0,0 +1,232 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ // Codex's lifecycle hooks use their own event and output shapes. Keep Claude's
6
+ // title updates and UI out of this adapter.
7
+ const local = (reason) => ({ decision: "block", reason });
8
+ const MARKETPLACE = "webinson-peer-codex";
9
+ const PLUGIN = `peer-thread-mark@${MARKETPLACE}`;
10
+ const setupFile = (peer) => path.join(peer.home, "codex-cli.json");
11
+
12
+ export function installCodex(peer, { client = false } = {}) {
13
+ if (peer.cli.includes(`${path.sep}_npx${path.sep}`)) {
14
+ throw new Error("Codex needs a lasting `peer`: run `npm install -g @webinson/peer`, then `peer install --codex`.");
15
+ }
16
+ const marketplace = fs.realpathSync(path.join(peer.mod, "codex"));
17
+ let clientPath = peer.readJson(setupFile(peer))?.client;
18
+ if (client) {
19
+ if (process.env.PEER_CODEX_CLIENT) {
20
+ clientPath = path.resolve(process.env.PEER_CODEX_CLIENT);
21
+ fs.accessSync(clientPath, fs.constants.X_OK);
22
+ if (!fs.statSync(clientPath).isFile()) throw new Error("PEER_CODEX_CLIENT must point to the Codex executable");
23
+ } else {
24
+ const checkout = path.resolve(peer.home, "codex-client");
25
+ execFileSync("bash", [path.join(marketplace, "client", "build.sh"), checkout], { stdio: "inherit" });
26
+ clientPath = path.join(checkout, "codex-rs", "target", "debug", "codex");
27
+ }
28
+ }
29
+
30
+ const binary = process.env.PEER_CODEX || (client ? clientPath : "codex");
31
+ const runCodex = (args, capture = false) => codex(args, { binary, capture });
32
+ const listed = runCodex(["plugin", "marketplace", "list", "--json"], true);
33
+ const previous = JSON.parse(listed).marketplaces?.find((item) => item.name === MARKETPLACE);
34
+ const moving = previous && previous.root !== marketplace;
35
+ if (moving && previous.marketplaceSource?.sourceType !== "local") {
36
+ throw new Error(`Codex marketplace ${MARKETPLACE} already uses a non-local source`);
37
+ }
38
+ if (moving) runCodex(["plugin", "marketplace", "remove", MARKETPLACE]);
39
+ let added = false;
40
+ try {
41
+ runCodex(["plugin", "marketplace", "add", marketplace]);
42
+ added = true;
43
+ runCodex(["plugin", "add", PLUGIN]);
44
+ peer.writeJson(setupFile(peer), { node: process.execPath, peer: peer.cli, client: clientPath });
45
+ } catch (error) {
46
+ if (moving) {
47
+ try {
48
+ if (added) runCodex(["plugin", "marketplace", "remove", MARKETPLACE]);
49
+ runCodex(["plugin", "marketplace", "add", previous.marketplaceSource.source]);
50
+ } catch {
51
+ console.error(`Could not restore the previous ${MARKETPLACE} marketplace.`);
52
+ }
53
+ }
54
+ throw error;
55
+ }
56
+ console.log("Codex: Peer installed. Review and trust its hooks in /hooks.");
57
+ if (client) console.log("Start the Peer terminal with `peer-codex`.");
58
+ }
59
+
60
+ export function uninstallCodex(peer) {
61
+ const clientPath = peer.readJson(setupFile(peer))?.client;
62
+ const binary = process.env.PEER_CODEX || (clientPath && fs.existsSync(clientPath) ? clientPath : "codex");
63
+ codex(["plugin", "remove", PLUGIN], { binary });
64
+ fs.rmSync(setupFile(peer), { force: true });
65
+ console.log("Codex: Peer's plugin removed.");
66
+ }
67
+
68
+ function codex(args, { binary, capture = false }) {
69
+ return execFileSync(binary, args, {
70
+ stdio: capture ? ["ignore", "pipe", "inherit"] : "inherit",
71
+ encoding: "utf8",
72
+ timeout: 60_000,
73
+ });
74
+ }
75
+
76
+ const modeOf = (peer, session) => {
77
+ const mark = peer.readJson(peer.markerOf(session));
78
+ return mark?.workspace ? mark.mode === "jellyfish" ? "jellyfish" : "elephant" : "inactive";
79
+ };
80
+
81
+ // The capture hook sees only these windows. Closing one freezes the transcript
82
+ // that OpenViking can recover on a later resume, even if a worker missed a turn.
83
+ function recordMode(peer, session, before, after) {
84
+ if (before === after) return;
85
+ const file = path.join(peer.home, "codex-capture", `${session}.json`);
86
+ const saved = peer.readJson(file);
87
+ const windows = Array.isArray(saved) ? saved : [];
88
+ const now = Date.now();
89
+ const last = windows.at(-1);
90
+ if (last?.end === null) {
91
+ last.end = now;
92
+ if (before === "inactive" && last.mode === "elephant") {
93
+ // Logout or expiry may have removed a marker without a final hook. Only
94
+ // keep the old window through its last known opt-in projection.
95
+ const snapshot = path.join(peer.home, "codex-capture", "rollouts", `${session}.jsonl`);
96
+ const observed = fs.statSync(snapshot, { throwIfNoEntry: false })?.mtimeMs;
97
+ last.end = Math.max(last.start, Math.min(now, Math.ceil(observed ?? last.start)));
98
+ }
99
+ }
100
+ if (after !== "inactive") windows.push({ mode: after, start: now, end: null });
101
+ peer.writeJson(file, windows);
102
+ }
103
+
104
+ async function setMode(peer, id, cwd, target, { toggle = false, working = false } = {}) {
105
+ const session = `codex-${id}`;
106
+ const before = modeOf(peer, session);
107
+ const mode = toggle && before === target ? "inactive" : target;
108
+ const saved = peer.signedIn();
109
+ recordMode(peer, session, before, mode);
110
+ if (mode === "inactive") return peer.stopSharing(saved, session, cwd, "codex");
111
+ return (await peer.shareThread(saved, session, cwd, working, "codex", mode)).said;
112
+ }
113
+
114
+ const countMemories = ({ saves }) => {
115
+ if (!saves) return null;
116
+ const items = saves.flatMap((save) => save.items);
117
+ const updated = new Set(items.filter((item) => item.change === "updated" && !item.systemText).map((item) => `${item.type}:${item.name}`));
118
+ return items.filter((item) => item.change === "added" || item.systemText).length + updated.size;
119
+ };
120
+
121
+ export async function codexControl(input, peer) {
122
+ const { action, session_id: id, cwd, query = "" } = input;
123
+ const session = `codex-${id}`;
124
+ if (!peer.validSession(id) || !peer.validSession(session)) throw new Error("invalid Codex session id");
125
+
126
+ let output;
127
+ let memoryCount;
128
+ let memoryPage;
129
+ switch (action) {
130
+ case "status":
131
+ memoryCount = await peer.threadMemories(id, true).then(countMemories).catch(() => null);
132
+ break;
133
+ case "elephant":
134
+ case "jellyfish":
135
+ output = await setMode(peer, id, cwd, action, { toggle: true });
136
+ break;
137
+ case "start":
138
+ output = await setMode(peer, id, cwd, modeOf(peer, session) === "jellyfish" ? "jellyfish" : "elephant", { working: input.working === true });
139
+ break;
140
+ case "stop":
141
+ output = await setMode(peer, id, cwd, "inactive");
142
+ break;
143
+ case "list":
144
+ output = await peer.listText(peer.signedIn());
145
+ break;
146
+ case "thread": {
147
+ const memories = await peer.threadMemories(id, true);
148
+ output = peer.threadMemoryText(memories);
149
+ memoryCount = countMemories(memories);
150
+ break;
151
+ }
152
+ case "all": {
153
+ const filters = peer.memoryQuery(query.split(/\s+/).filter(Boolean), cwd || process.cwd());
154
+ memoryPage = { ...await peer.memoryPage(filters), here: filters.here };
155
+ break;
156
+ }
157
+ default:
158
+ throw new Error("unknown Codex Peer action");
159
+ }
160
+ return { mode: modeOf(peer, session), output, memoryCount, memoryPage };
161
+ }
162
+
163
+ export async function codexHook(input, peer) {
164
+ const { hook_event_name: event, session_id: id, cwd } = input;
165
+ const session = `codex-${id}`;
166
+ if (input.agent_id || !peer.validSession(id) || !peer.validSession(session)) return;
167
+
168
+ switch (event) {
169
+ case "SessionStart":
170
+ peer.forgetOldThreads();
171
+ await peer.teamHook("team-start", { ...input, session_id: session });
172
+ if (input.source === "resume") await peer.reportQuietly(session, "idle", cwd, "codex");
173
+ return;
174
+ case "Stop":
175
+ case "Interrupt":
176
+ await peer.reportQuietly(session, "idle", cwd, "codex", 1500);
177
+ return;
178
+ case "SessionEnd":
179
+ await peer.reportQuietly(session, "stopped", cwd, "codex", 1500);
180
+ return;
181
+ case "UserPromptSubmit":
182
+ return promptHook(String(input.prompt ?? "").trim(), id, cwd, peer);
183
+ }
184
+ }
185
+
186
+ async function promptHook(prompt, id, cwd, peer) {
187
+ const session = `codex-${id}`;
188
+ // The text form also works in clients that handle slash commands before submitting a turn.
189
+ const all = /^(?:\/peer-all|peer:\s+all)(?:\s+([\s\S]*))?$/.exec(prompt);
190
+ if (all) {
191
+ const query = peer.memoryQuery((all[1] ?? "").split(/\s+/).filter(Boolean), cwd || process.cwd());
192
+ return local(await peer.memoryText(query).catch((error) => `Peer: your memories did not load (${error.message})`));
193
+ }
194
+ if (/^(?:\/peer-list|peer:\s+list)$/.test(prompt)) {
195
+ try {
196
+ return local(await peer.listText(peer.signedIn()));
197
+ } catch (error) {
198
+ return local(`Peer: ${error.message}`);
199
+ }
200
+ }
201
+ if (/^(?:\/peer-thread|peer:\s+thread)$/.test(prompt)) {
202
+ try {
203
+ return local(peer.threadMemoryText(await peer.threadMemories(id, true)));
204
+ } catch (error) {
205
+ return local(`Peer: your thread memories did not load (${error.message})`);
206
+ }
207
+ }
208
+
209
+ const share = /^(?:\/peer|peer:\s+start)(?:\s+([\s\S]*))?$/.exec(prompt);
210
+ const message = share?.[1]?.trim() ?? "";
211
+ const stop = (share && message === "stop") || /^peer:\s+stop$/.test(prompt);
212
+ if (share || stop) {
213
+ try {
214
+ if (stop) return local(await setMode(peer, id, cwd, "inactive"));
215
+ const mode = modeOf(peer, session);
216
+ const said = await setMode(peer, id, cwd, mode === "inactive" ? "elephant" : mode, { working: Boolean(message) });
217
+ if (!message) return local(said);
218
+ return {
219
+ hookSpecificOutput: {
220
+ hookEventName: "UserPromptSubmit",
221
+ additionalContext: `${said}\nThe user included a message after their Peer command. Continue with that message.`,
222
+ },
223
+ };
224
+ } catch (error) {
225
+ return local(`Peer: ${error.message}`);
226
+ }
227
+ }
228
+ await peer.reportQuietly(session, "working", cwd, "codex");
229
+ if (modeOf(peer, session) === "elephant") {
230
+ return peer.teamHook("team-prompt", { session_id: session, cwd, prompt });
231
+ }
232
+ }
package/bin/peer.mjs CHANGED
@@ -30,8 +30,8 @@ const MARKETPLACE = "webinson-peer";
30
30
 
31
31
  const HELP = `peer: share chosen agent threads with your team
32
32
 
33
- peer login [email] [--hub <url>] [--workspace <slug>]
34
- sign in once and set up Claude Code (/peer, /peer stop, /peer-list)
33
+ peer login [email] [--hub <url>] [--workspace <slug>] [--codex [--client]]
34
+ sign in once and set up Claude Code or Codex
35
35
  peer list people in the workspace, their shared threads and what they work on
36
36
  peer status who you are, the workspace and the threads this computer shares
37
37
  peer doctor why Peer's hook did not answer /peer
@@ -43,8 +43,9 @@ const HELP = `peer: share chosen agent threads with your team
43
43
  peer all [words] [here] [--newest | --oldest] [page N]
44
44
  your own memories in your personal OpenViking, or the ones with these words
45
45
  (full text); here: only this repository's. /peer-all in Claude Code
46
- peer install set up Claude Code again (hooks, the two commands and the thread mark)
47
- peer uninstall remove them from Claude Code
46
+ peer install [--codex [--client]]
47
+ set up the thread mark; --client also builds the Peer Codex terminal
48
+ peer uninstall [--codex] remove the integration from Claude Code or Codex
48
49
  peer logout forget the sign-in on this computer
49
50
 
50
51
  In Claude Code: /peer shares the thread (/peer <message> also sends the message on), /peer stop
@@ -133,8 +134,8 @@ function workingOn(cwd) {
133
134
  return { repository, branch: branch && branch !== "HEAD" ? branch : undefined, repoKey };
134
135
  }
135
136
 
136
- async function report(saved, session, state, cwd) {
137
- await call(saved, "PUT", `/workspaces/${saved.workspace}/agents/${session}`, { agent: "claude-code", state, ...workingOn(cwd) }, HUB_TIMEOUT_MS);
137
+ async function report(saved, session, state, cwd, agent = "claude-code", timeoutMs = HUB_TIMEOUT_MS) {
138
+ await call(saved, "PUT", `/workspaces/${saved.workspace}/agents/${session}`, { agent, state, ...workingOn(cwd) }, timeoutMs);
138
139
  }
139
140
 
140
141
  // ---- the list ----
@@ -235,6 +236,28 @@ function log(line) {
235
236
  } catch {}
236
237
  }
237
238
 
239
+ async function shareThread(saved, session, cwd, working, agent, mode) {
240
+ writeJson(markerOf(session), { workspace: saved.workspace, sharedAt: new Date().toISOString(), mode });
241
+ const where = workingOn(cwd);
242
+ const what = [where.repository, where.branch].filter(Boolean).map(plain).join(" @ ");
243
+ let said = `Peer: this thread is shared in ${saved.workspace}${what ? ` as ${what}` : ""}. Teammates see its repository, branch and state, nothing of the conversation. /peer stop ends it.`;
244
+ try {
245
+ await report(saved, session, working ? "working" : "idle", cwd, agent);
246
+ } catch (error) {
247
+ said = `Peer: shared, but the hub did not answer yet (${error.message}). The next turn tries again.`;
248
+ }
249
+ return { said, what };
250
+ }
251
+
252
+ async function stopSharing(saved, session, cwd, agent) {
253
+ const marker = markerOf(session);
254
+ const shared = readJson(marker);
255
+ fs.rmSync(marker, { force: true });
256
+ if (!shared?.workspace) return "Peer: this thread was not shared.";
257
+ await report({ ...saved, workspace: shared.workspace }, session, "stopped", cwd, agent).catch(() => {});
258
+ return "Peer: this thread is no longer shared.";
259
+ }
260
+
238
261
  async function hook(event) {
239
262
  const input = await readStdin();
240
263
  const session = input.session_id;
@@ -268,19 +291,10 @@ async function hook(event) {
268
291
  return block(`Peer: ${error.message}`);
269
292
  }
270
293
  if (argument === "stop") {
271
- fs.rmSync(marker, { force: true });
272
- if (!shared) return block("Peer: this thread was not shared.", retitle(input, false));
273
- await report(saved, session, "stopped", input.cwd).catch(() => {});
274
- return block("Peer: this thread is no longer shared.", retitle(input, false));
294
+ return block(await stopSharing(saved, session, input.cwd), retitle(input, false));
275
295
  }
276
296
  // `/peer <message>` shares the thread and lets the message through: the command hands it to the model.
277
- writeJson(marker, { workspace: saved.workspace, sharedAt: new Date().toISOString() });
278
- const where = workingOn(input.cwd);
279
- const what = [where.repository, where.branch].filter(Boolean).join(" @ ");
280
- const sharedText = `Peer: this thread is shared in ${saved.workspace}${what ? ` as ${what}` : ""}. Teammates see its repository, branch and state, nothing of the conversation. /peer stop ends it.`;
281
- let hubError;
282
- await report(saved, session, argument ? "working" : "idle", input.cwd).catch((error) => (hubError = error));
283
- const said = hubError ? `Peer: shared, but the hub did not answer yet (${hubError.message}). The next turn tries again.` : sharedText;
297
+ const { said, what } = await shareThread(saved, session, input.cwd, Boolean(argument));
284
298
  const sessionTitle = retitle(input, true, what);
285
299
  // A bare /peer reaches the agent too, which confirms in one line: a block would show the person Claude
286
300
  // Code's "hook blocked your prompt" warning for what went right.
@@ -319,11 +333,11 @@ function forgetOldThreads() {
319
333
  }
320
334
 
321
335
  // A turn never waits on or fails because of the hub: a report that does not go through is dropped.
322
- async function reportQuietly(session, state, cwd) {
336
+ async function reportQuietly(session, state, cwd, agent, timeoutMs) {
323
337
  const saved = config();
324
338
  const marker = readJson(markerOf(session));
325
339
  if (!saved?.session || !marker?.workspace) return;
326
- await report({ ...saved, workspace: marker.workspace }, session, state, cwd).catch(() => {});
340
+ await report({ ...saved, workspace: marker.workspace }, session, state, cwd, agent, timeoutMs).catch(() => {});
327
341
  }
328
342
 
329
343
  // ---- Claude Code setup ----
@@ -458,6 +472,10 @@ function option(args, name) {
458
472
  }
459
473
 
460
474
  async function login(args) {
475
+ const forCodex = args.includes("--codex");
476
+ const client = args.includes("--client");
477
+ if (client && !forCodex) throw new Error("--client requires --codex");
478
+ args = args.filter((arg) => arg !== "--codex" && arg !== "--client");
461
479
  const hub = (option(args, "--hub") ?? process.env.PEER_HUB ?? config()?.hub ?? DEFAULT_HUB).replace(/\/+$/, "");
462
480
  let workspace = option(args, "--workspace");
463
481
  const ask = readline.createInterface({ input: process.stdin, output: process.stderr });
@@ -493,7 +511,8 @@ async function login(args) {
493
511
  } finally {
494
512
  ask.close();
495
513
  }
496
- install();
514
+ if (forCodex) (await import("./codex-thread-mark.mjs")).installCodex(codexPeer, { client });
515
+ else install();
497
516
  }
498
517
 
499
518
  async function logout() {
@@ -823,19 +842,20 @@ function threadUses(session, prompts) {
823
842
  return uses.slice(-200);
824
843
  }
825
844
 
826
- async function threadMemories(session) {
845
+ async function threadMemories(session, codex = false) {
827
846
  const ov = readJson(OV_CONF);
828
847
  if (!ov?.url) return { connected: false, reason: `no OpenViking client config at ${OV_CONF}` };
829
- const base = `viking://user/${ov.user}/sessions/cc-${session}`;
848
+ const base = `viking://user/${ov.user}/sessions/${codex ? "cx" : "cc"}-${session}`;
849
+ const uses = (prompts) => codex ? [] : threadUses(session, prompts);
830
850
  let archives;
831
851
  try {
832
852
  archives = await ovRead(ov, `/fs/ls?uri=${encodeURIComponent(`${base}/history`)}`);
833
853
  } catch (error) {
834
- if (/not found|no such|404/i.test(error.message)) return { connected: true, saves: [], uses: threadUses(session, transcriptPrompts(session)) };
854
+ if (/not found|no such|404/i.test(error.message)) return { connected: true, saves: [], uses: uses(codex ? [] : transcriptPrompts(session)) };
835
855
  throw error;
836
856
  }
837
857
  const saves = [];
838
- const prompts = transcriptPrompts(session);
858
+ const prompts = codex ? [] : transcriptPrompts(session);
839
859
  for (const archive of archives.filter((entry) => entry.isDir)) {
840
860
  let diff;
841
861
  try {
@@ -872,7 +892,15 @@ async function threadMemories(session) {
872
892
  items.forEach((item, index) => (item.body = String(bodies[index] ?? "").slice(0, 3000)));
873
893
  saves.push({ at: diff.extracted_at ?? archive.modTime, items });
874
894
  }
875
- return { connected: true, saves: saves.sort((a, b) => String(b.at).localeCompare(String(a.at))), uses: threadUses(session, prompts) };
895
+ return { connected: true, saves: saves.sort((a, b) => String(b.at).localeCompare(String(a.at))), uses: uses(prompts) };
896
+ }
897
+
898
+ function threadMemoryText(memories) {
899
+ if (!memories.connected) return `Peer: ${memories.reason}`;
900
+ if (!memories.saves.length) return "This thread has not saved anything to memory yet.";
901
+ return memories.saves.flatMap((save) => [save.at, ...save.items.map((item) =>
902
+ ` ${item.change} ${item.type} "${item.name}": ${item.lines.join(" / ")}${item.systemText ? " [holds Claude Code's own text]" : ""}`,
903
+ )]).join("\n");
876
904
  }
877
905
 
878
906
  // ---- team memory: teammates' published project knowledge at the start of a thread ----
@@ -973,8 +1001,7 @@ function changesSince(cwd, paths, date) {
973
1001
  return log ? log.split("\n").filter(Boolean).length : 0;
974
1002
  }
975
1003
 
976
- async function teamHook(event) {
977
- const input = await readStdin();
1004
+ async function teamHook(event, input) {
978
1005
  const team = readJson(TEAM_FILE);
979
1006
  const session = input.session_id;
980
1007
  if (!team || !validSession(session)) return;
@@ -1033,7 +1060,7 @@ async function teamHook(event) {
1033
1060
  ...entries,
1034
1061
  "</teammates-project-knowledge>",
1035
1062
  ].join("\n");
1036
- answer({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: block } });
1063
+ return { hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: block } };
1037
1064
  }
1038
1065
 
1039
1066
  // ---- your own memory: what your personal OpenViking remembered, listed and searched ----
@@ -1191,6 +1218,13 @@ async function memoryText(query) {
1191
1218
 
1192
1219
  // ---- main ----
1193
1220
 
1221
+ const codexPeer = {
1222
+ home: HOME, cli: SELF, mod: MOD_DIR,
1223
+ validSession, readJson, writeJson, markerOf, config, forgetOldThreads,
1224
+ reportQuietly, signedIn, shareThread, stopSharing, listText, memoryQuery, memoryPage, memoryText,
1225
+ threadMemories, threadMemoryText, teamHook,
1226
+ };
1227
+
1194
1228
  const [command, ...args] = process.argv.slice(2);
1195
1229
  try {
1196
1230
  switch (command) {
@@ -1211,16 +1245,10 @@ try {
1211
1245
  case "thread": {
1212
1246
  // `peer thread --json <session id>` is for the thread-mark mod's pane.
1213
1247
  const session = args.find((arg) => !arg.startsWith("--"));
1214
- if (!validSession(session)) throw new Error("usage: peer thread [--json] <claude code session id>");
1215
- const memories = await threadMemories(session);
1248
+ if (!validSession(session)) throw new Error("usage: peer thread [--json] [--codex] <session id>");
1249
+ const memories = await threadMemories(session, args.includes("--codex"));
1216
1250
  if (args.includes("--json")) console.log(JSON.stringify(memories));
1217
- else if (!memories.connected) console.log(`Peer: ${memories.reason}`);
1218
- else if (!memories.saves.length) console.log("This thread has not saved anything to memory yet.");
1219
- else
1220
- for (const save of memories.saves) {
1221
- console.log(save.at);
1222
- for (const item of save.items) console.log(` ${item.change} ${item.type} "${item.name}": ${item.lines.join(" / ")}${item.systemText ? " [holds Claude Code's own text]" : ""}`);
1223
- }
1251
+ else console.log(threadMemoryText(memories));
1224
1252
  break;
1225
1253
  }
1226
1254
  case "status":
@@ -1231,14 +1259,31 @@ try {
1231
1259
  break;
1232
1260
  case "install":
1233
1261
  signedIn();
1234
- install();
1262
+ if (args.includes("--codex")) {
1263
+ (await import("./codex-thread-mark.mjs")).installCodex(codexPeer, { client: args.includes("--client") });
1264
+ } else if (args.includes("--client")) throw new Error("--client requires --codex");
1265
+ else install();
1235
1266
  break;
1236
1267
  case "uninstall":
1237
- uninstall();
1268
+ if (args.includes("--codex")) (await import("./codex-thread-mark.mjs")).uninstallCodex(codexPeer);
1269
+ else uninstall();
1270
+ break;
1271
+ case "codex-control":
1272
+ answer(await (await import("./codex-thread-mark.mjs")).codexControl(await readStdin(), codexPeer));
1238
1273
  break;
1239
1274
  case "hook":
1240
1275
  // A hook never breaks the person's session: whatever goes wrong, it exits 0.
1241
- await (args[0]?.startsWith("team-") ? teamHook(args[0]) : hook(args[0])).catch((error) => log(`hook ${args[0]} failed: ${error?.message ?? error}`));
1276
+ await (async () => {
1277
+ if (args[0] === "codex") {
1278
+ const { codexHook } = await import("./codex-thread-mark.mjs");
1279
+ const output = await codexHook(await readStdin(), codexPeer);
1280
+ if (output) answer(output);
1281
+ } else if (args[0]?.startsWith("team-")) {
1282
+ const output = await teamHook(args[0], await readStdin());
1283
+ if (output) answer(output);
1284
+ }
1285
+ else await hook(args[0]);
1286
+ })().catch((error) => log(`hook ${args[0]} failed: ${error?.message ?? error}`));
1242
1287
  break;
1243
1288
  case "all": {
1244
1289
  // `peer all --json …` is for the thread-mark mod's pane; without a personal OpenViking it says so.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "webinson-peer-codex",
3
+ "interface": { "displayName": "Peer" },
4
+ "plugins": [
5
+ {
6
+ "name": "peer-thread-mark",
7
+ "source": { "source": "local", "path": "./peer-thread-mark" },
8
+ "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" },
9
+ "category": "Productivity"
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1 @@
1
+ *.patch whitespace=-blank-at-eol