@webinson/peer 0.4.22 → 0.4.24
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 +12 -9
- package/bin/codex-native.mjs +73 -0
- package/bin/codex-thread-mark.mjs +6 -6
- package/bin/peer-codex.mjs +45 -0
- package/bin/peer.mjs +15 -8
- package/mod/codex/client/README.md +29 -5
- package/mod/codex/client/build.sh +25 -10
- package/mod/codex/client/pack.sh +80 -0
- package/mod/codex/client/run.sh +2 -19
- package/mod/peer-thread-mark/.claude-plugin/plugin.json +1 -1
- package/mod/peer-thread-mark/hooks/register.tsx +27 -0
- package/mod/peer-thread-mark/tests/modes.test.tsx +83 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@ and the commands `/peer` and `/peer-list` in `~/.claude/commands`.
|
|
|
14
14
|
|
|
15
15
|
In a Claude Code thread:
|
|
16
16
|
|
|
17
|
-
- `/peer` shares this thread in your workspace.
|
|
17
|
+
- `/peer` shares this thread in your workspace. Its turn only confirms the sharing and carries no memory at all,
|
|
18
|
+
so a thread you then switch to Jellyfish holds none from the start.
|
|
19
|
+
- `/peer jellyfish` and `/peer elephant` share it in that memory mode at once.
|
|
18
20
|
- `/peer <message>` shares it and passes the message on to the agent as your prompt.
|
|
19
21
|
- `/peer stop` ends sharing. Quitting or restarting Claude Code only pauses it: the thread shows as stopped and
|
|
20
22
|
is shared again when the session is resumed. `/clear` and signing out of Claude Code end it.
|
|
@@ -25,7 +27,9 @@ In a Claude Code thread:
|
|
|
25
27
|
thread goes, on top of Claude Code's own memory. It saves only the turns from the moment it was chosen.
|
|
26
28
|
- **Inactive** (not shared): Claude Code's own memory alone; PeerMemory stays off.
|
|
27
29
|
- **Use Jellyfish** (shared): no memory at all, neither PeerMemory nor Claude Code's own (`MEMORY.md`).
|
|
28
|
-
|
|
30
|
+
The agent cannot read, search or change Claude Code's memory files with a tool either.
|
|
31
|
+
`CLAUDE.md` instructions stay. Claude Code puts `MEMORY.md` into a thread's first message, so a thread meant to
|
|
32
|
+
hold none starts with `/peer` (or `/peer jellyfish`): a first message with work in it already carries it.
|
|
29
33
|
|
|
30
34
|
Pressing the lit mode again stops sharing (Inactive); pressing either while Inactive shares again. Every thread
|
|
31
35
|
that is not shared runs without OpenViking. What a thread was already given stays in it after a switch. The
|
|
@@ -99,11 +103,11 @@ peer-codex
|
|
|
99
103
|
If you already signed in to Peer, use `peer install --codex --client` in place of the login
|
|
100
104
|
command. Review and trust the Peer Thread Mark hooks in Codex's `/hooks` menu.
|
|
101
105
|
|
|
102
|
-
The `--client` setup
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
launcher uses your existing Codex settings and sign-in with its
|
|
106
|
-
moves the
|
|
106
|
+
The `--client` setup downloads the matching prebuilt client from npm into
|
|
107
|
+
`~/.peer/codex-clients`. It supports macOS, Linux, and Windows on x64 and ARM64,
|
|
108
|
+
and requires Node.js and npm. Set `PEER_CODEX_CLIENT` to use a custom build.
|
|
109
|
+
The `peer-codex` launcher uses your existing Codex settings and sign-in with its
|
|
110
|
+
own app server. `PEER_HOME` moves the client cache and Peer state.
|
|
107
111
|
|
|
108
112
|
For the plugin alone, omit `--client`. The Codex plugin lives under [`mod/codex`](mod/codex)
|
|
109
113
|
in its own marketplace.
|
|
@@ -113,8 +117,7 @@ In stock Codex, send `peer: start`, `peer: stop`, `peer: list`, `peer: thread`,
|
|
|
113
117
|
This plugin uses the [Codex hook API](https://learn.chatgpt.com/docs/hooks); stock Codex does
|
|
114
118
|
not expose a composer band or memory switches to plugins.
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
`bash mod/codex/client/run.sh` from `cli/`.
|
|
120
|
+
For a custom build, see [building the client](mod/codex/client/README.md).
|
|
118
121
|
|
|
119
122
|
The terminal band shows the thread's memory count, **Use Elephant**, **Use Jellyfish**, and
|
|
120
123
|
**Active** or **Inactive**. Press the lit mode again to stop sharing. Use `/peer-mode` for
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const codexTargets = {
|
|
6
|
+
"darwin-arm64": "aarch64-apple-darwin",
|
|
7
|
+
"darwin-x64": "x86_64-apple-darwin",
|
|
8
|
+
"linux-arm64": "aarch64-unknown-linux-musl",
|
|
9
|
+
"linux-x64": "x86_64-unknown-linux-musl",
|
|
10
|
+
"win32-arm64": "aarch64-pc-windows-msvc",
|
|
11
|
+
"win32-x64": "x86_64-pc-windows-msvc",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const codexPackageName = (platform) => `@webinson/peer-codex-${platform}`;
|
|
15
|
+
export const codexExecutable = (platform) => platform === "win32" ? "codex.exe" : "codex";
|
|
16
|
+
export const codexNativeVersion = "0.4.18";
|
|
17
|
+
|
|
18
|
+
function installedClient(directory, target) {
|
|
19
|
+
const bin = path.join(directory, "vendor", target, "bin");
|
|
20
|
+
const suffix = process.platform === "win32" ? ".exe" : "";
|
|
21
|
+
const client = path.join(bin, codexExecutable(process.platform));
|
|
22
|
+
try {
|
|
23
|
+
for (const executable of [client, path.join(bin, `codex-code-mode-host${suffix}`)]) {
|
|
24
|
+
if (!fs.statSync(executable).isFile()) return null;
|
|
25
|
+
fs.accessSync(executable, fs.constants.X_OK);
|
|
26
|
+
}
|
|
27
|
+
return client;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function installNativeCodex(peer) {
|
|
34
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
35
|
+
const target = codexTargets[platform];
|
|
36
|
+
if (!target) {
|
|
37
|
+
throw new Error(`No prebuilt Peer Codex for ${platform}. Set PEER_CODEX_CLIENT to a custom build.`);
|
|
38
|
+
}
|
|
39
|
+
const packageName = codexPackageName(platform);
|
|
40
|
+
const spec = `${packageName}@${codexNativeVersion}`;
|
|
41
|
+
const root = path.resolve(peer.home, "codex-clients");
|
|
42
|
+
const destination = path.join(root, `${platform}-${codexNativeVersion}`);
|
|
43
|
+
if (fs.existsSync(destination)) {
|
|
44
|
+
const existing = installedClient(destination, target);
|
|
45
|
+
if (existing) return existing;
|
|
46
|
+
throw new Error(`Incomplete Peer Codex install at ${destination}. Remove that directory and retry.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
50
|
+
const staging = fs.mkdtempSync(path.join(root, ".install-"));
|
|
51
|
+
try {
|
|
52
|
+
const args = [
|
|
53
|
+
"install", "--prefix", ".", "--global=false", "--ignore-scripts",
|
|
54
|
+
"--no-save", "--no-package-lock", "--no-audit", "--no-fund", spec,
|
|
55
|
+
];
|
|
56
|
+
const options = { cwd: staging, stdio: "inherit", timeout: 600_000 };
|
|
57
|
+
if (process.platform === "win32") {
|
|
58
|
+
execFileSync(process.env.ComSpec || "cmd.exe", ["/d", "/c", "npm", ...args], options);
|
|
59
|
+
} else {
|
|
60
|
+
execFileSync("npm", args, options);
|
|
61
|
+
}
|
|
62
|
+
const downloaded = path.join(staging, "node_modules", packageName);
|
|
63
|
+
if (!installedClient(downloaded, target)) throw new Error(`${spec} is missing its executables`);
|
|
64
|
+
try {
|
|
65
|
+
fs.renameSync(downloaded, destination);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (!installedClient(destination, target)) throw error;
|
|
68
|
+
}
|
|
69
|
+
return installedClient(destination, target);
|
|
70
|
+
} finally {
|
|
71
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { installNativeCodex } from "./codex-native.mjs";
|
|
4
5
|
|
|
5
6
|
// Codex's lifecycle hooks use their own event and output shapes. Keep Claude's
|
|
6
7
|
// title updates and UI out of this adapter.
|
|
@@ -21,14 +22,13 @@ export function installCodex(peer, { client = false } = {}) {
|
|
|
21
22
|
fs.accessSync(clientPath, fs.constants.X_OK);
|
|
22
23
|
if (!fs.statSync(clientPath).isFile()) throw new Error("PEER_CODEX_CLIENT must point to the Codex executable");
|
|
23
24
|
} else {
|
|
24
|
-
|
|
25
|
-
execFileSync("bash", [path.join(marketplace, "client", "build.sh"), checkout], { stdio: "inherit" });
|
|
26
|
-
clientPath = path.join(checkout, "codex-rs", "target", "debug", "codex");
|
|
25
|
+
clientPath = installNativeCodex(peer);
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
const binary = process.env.PEER_CODEX || (client ? clientPath : "codex");
|
|
31
|
-
const
|
|
30
|
+
const timeout = client ? 300_000 : 60_000;
|
|
31
|
+
const runCodex = (args, capture = false) => codex(args, { binary, capture, timeout });
|
|
32
32
|
const listed = runCodex(["plugin", "marketplace", "list", "--json"], true);
|
|
33
33
|
const previous = JSON.parse(listed).marketplaces?.find((item) => item.name === MARKETPLACE);
|
|
34
34
|
const moving = previous && previous.root !== marketplace;
|
|
@@ -65,11 +65,11 @@ export function uninstallCodex(peer) {
|
|
|
65
65
|
console.log("Codex: Peer's plugin removed.");
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function codex(args, { binary, capture = false }) {
|
|
68
|
+
function codex(args, { binary, capture = false, timeout = 60_000 }) {
|
|
69
69
|
return execFileSync(binary, args, {
|
|
70
70
|
stdio: capture ? ["ignore", "pipe", "inherit"] : "inherit",
|
|
71
71
|
encoding: "utf8",
|
|
72
|
-
timeout
|
|
72
|
+
timeout,
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { codexExecutable } from "./codex-native.mjs";
|
|
7
|
+
|
|
8
|
+
const home = process.env.PEER_HOME || path.join(os.homedir(), ".peer");
|
|
9
|
+
let configured;
|
|
10
|
+
try {
|
|
11
|
+
configured = JSON.parse(fs.readFileSync(path.join(home, "codex-cli.json"), "utf8"))?.client;
|
|
12
|
+
} catch {}
|
|
13
|
+
const client = process.env.PEER_CODEX_CLIENT
|
|
14
|
+
|| (typeof configured === "string" && path.isAbsolute(configured) ? configured : null)
|
|
15
|
+
|| path.join(home, "codex-client", "codex-rs", "target", "debug", codexExecutable(process.platform));
|
|
16
|
+
try {
|
|
17
|
+
fs.accessSync(client, fs.constants.X_OK);
|
|
18
|
+
} catch {
|
|
19
|
+
console.error("Set up the Peer Codex terminal with peer install --codex --client.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A stock daemon would use the stock memory policy.
|
|
24
|
+
const args = ["--no-daemon", ...process.argv.slice(2)];
|
|
25
|
+
const env = { ...process.env, PEER_CODEX_THREAD_MARK: "1" };
|
|
26
|
+
if (process.platform !== "win32" && typeof process.execve === "function") {
|
|
27
|
+
process.on("SIGXFSZ", () => {});
|
|
28
|
+
process.execve(client, [client, ...args], env);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const child = spawn(client, args, { stdio: "inherit", env });
|
|
32
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
33
|
+
const forward = (signal) => {
|
|
34
|
+
if (!child.killed) child.kill(signal);
|
|
35
|
+
};
|
|
36
|
+
for (const signal of signals) process.on(signal, forward);
|
|
37
|
+
child.on("error", (error) => {
|
|
38
|
+
console.error(`Could not start Peer Codex: ${error.message}`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
});
|
|
41
|
+
child.on("exit", (code, signal) => {
|
|
42
|
+
for (const name of signals) process.off(name, forward);
|
|
43
|
+
process.exitCode = code ?? (signal ? 128 + os.constants.signals[signal] : 1);
|
|
44
|
+
if (signal && process.platform !== "win32") process.kill(process.pid, signal);
|
|
45
|
+
});
|
package/bin/peer.mjs
CHANGED
|
@@ -46,12 +46,12 @@ const HELP = `peer: share chosen agent threads with your team
|
|
|
46
46
|
your own memories in your personal OpenViking, or the ones with these words
|
|
47
47
|
(full text); here: only this repository's. /peer-all in Claude Code
|
|
48
48
|
peer install [--codex [--client]]
|
|
49
|
-
set up the thread mark; --client also
|
|
49
|
+
set up the thread mark; --client also installs the Peer Codex terminal
|
|
50
50
|
peer uninstall [--codex] remove the integration from Claude Code or Codex
|
|
51
51
|
peer logout forget the sign-in on this computer
|
|
52
52
|
|
|
53
|
-
In Claude Code: /peer shares the thread (/peer <message> also sends the message on
|
|
54
|
-
ends it, /peer-list shows the list.
|
|
53
|
+
In Claude Code: /peer shares the thread (/peer <message> also sends the message on, /peer jellyfish
|
|
54
|
+
or /peer elephant picks its memory mode), /peer stop ends it, /peer-list shows the list.
|
|
55
55
|
Only threads where /peer was typed are shared, and only their repository, branch, state and the
|
|
56
56
|
paths of the files they edit: never prompts, answers, summaries or file contents.`;
|
|
57
57
|
|
|
@@ -307,13 +307,16 @@ async function hook(event) {
|
|
|
307
307
|
if (argument === "stop") {
|
|
308
308
|
return block(await stopSharing(saved, session, input.cwd), retitle(input, false));
|
|
309
309
|
}
|
|
310
|
+
// `/peer jellyfish` and `/peer elephant` share it like a bare /peer; the band's mod takes the mode from them.
|
|
311
|
+
const mode = /^(jellyfish|elephant)$/i.exec(argument)?.[1]?.toLowerCase();
|
|
312
|
+
const message = mode ? "" : argument;
|
|
310
313
|
// `/peer <message>` shares the thread and lets the message through: the command hands it to the model.
|
|
311
|
-
const { said, what } = await shareThread(saved, session, input.cwd, Boolean(
|
|
314
|
+
const { said, what } = await shareThread(saved, session, input.cwd, Boolean(message));
|
|
312
315
|
const sessionTitle = retitle(input, true, what);
|
|
313
316
|
// A bare /peer reaches the agent too, which confirms in one line: a block would show the person Claude
|
|
314
317
|
// Code's "hook blocked your prompt" warning for what went right.
|
|
315
|
-
const note =
|
|
316
|
-
log(
|
|
318
|
+
const note = message ? said : `${said}\n${mode ? MODE_NOTE(mode) : ALONE_NOTE}`;
|
|
319
|
+
log(message ? "answered /peer with a message: shared, message passed on" : `answered /peer${mode ? ` ${mode}` : ""}: shared, the agent confirms`);
|
|
317
320
|
return answer({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: `${SHARED_NOTE}\n${note}`, ...(sessionTitle ? { sessionTitle } : {}) } });
|
|
318
321
|
}
|
|
319
322
|
if (shared) await reportQuietly(session, "working", input.cwd);
|
|
@@ -366,10 +369,14 @@ const SHARED_NOTE = "[Peer's hook ran]";
|
|
|
366
369
|
const ALONE_NOTE =
|
|
367
370
|
"The person typed /peer alone. Reply with only that confirmation, in one or two short lines in the person's language: where the thread is shared, that teammates see its repository, branch, state and the paths of the files it edits but never the conversation, and that /peer stop ends it. Do nothing else and use no tools.";
|
|
368
371
|
|
|
372
|
+
// `/peer jellyfish` or `/peer elephant`: the same confirmation, with the mode the thread now uses.
|
|
373
|
+
const MODE_NOTE = (mode) =>
|
|
374
|
+
`The person typed /peer ${mode}: it picks the thread's memory mode and is not a message to work on. Reply with only that confirmation, in one or two short lines in the person's language: where the thread is shared, that teammates see its repository, branch, state and the paths of the files it edits but never the conversation, that the thread uses ${mode === "jellyfish" ? "Jellyfish (no memory)" : "Elephant (PeerMemory and Claude Code's own memory)"}, and that /peer stop ends it. Do nothing else and use no tools.`;
|
|
375
|
+
|
|
369
376
|
const COMMAND_FILES = {
|
|
370
377
|
"peer.md": `---
|
|
371
|
-
description: Share this thread on Peer (/peer), share it and go on (/peer <message>), or stop sharing it (/peer stop)
|
|
372
|
-
argument-hint: "[message | stop]"
|
|
378
|
+
description: Share this thread on Peer (/peer), with a memory mode (/peer jellyfish, /peer elephant), share it and go on (/peer <message>), or stop sharing it (/peer stop)
|
|
379
|
+
argument-hint: "[message | jellyfish | elephant | stop]"
|
|
373
380
|
---
|
|
374
381
|
$ARGUMENTS
|
|
375
382
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Peer for the Codex terminal
|
|
2
2
|
|
|
3
3
|
The sibling Codex plugin can report thread activity and answer commands with stock Codex.
|
|
4
|
-
`
|
|
5
|
-
|
|
4
|
+
`peer install --codex --client` installs the native npm package for the current OS and
|
|
5
|
+
architecture. `peer-codex` runs that client with its own app server so it uses the
|
|
6
|
+
Peer memory policy.
|
|
6
7
|
|
|
7
8
|
`src/` contains the Peer modules. The two patches wire them into the pinned Codex source:
|
|
8
9
|
`0001-memory.patch` controls plugin availability, capture and native memory; `0002-terminal.patch`
|
|
@@ -12,9 +13,32 @@ capture windows. Update the Codex pin, V8 version, checksums and patches togethe
|
|
|
12
13
|
The patches include context from OpenAI Codex, distributed under Apache-2.0.
|
|
13
14
|
See [CODEX-LICENSE](CODEX-LICENSE) and [CODEX-NOTICE](CODEX-NOTICE).
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
## Building and packing a release
|
|
17
|
+
|
|
18
|
+
`build.sh` takes the path of a checkout to create or reuse. It needs Git, Rust/Cargo,
|
|
19
|
+
`curl`, `shasum`, and a C/C++ toolchain. For a custom build, run it and set
|
|
20
|
+
`PEER_CODEX_CLIENT` to the resulting executable when running `peer install --codex --client`.
|
|
21
|
+
|
|
22
|
+
Set `PEER_CODEX_BUILD_PROFILE=release` for an npm build and set
|
|
23
|
+
`CARGO_PROFILE_RELEASE_STRIP=symbols` before compiling. Set `PEER_CODEX_BUILD_TARGET`
|
|
24
|
+
to build for a specific Rust target with the matching Rust standard library and C toolchain.
|
|
25
|
+
For cross-compiling, `PEER_CODEX_CARGO_COMMAND` can select `zigbuild` or `xwin`.
|
|
26
|
+
For Linux musl builds, use the pinned upstream release's musl tools and set
|
|
27
|
+
`CODEX_BWRAP_SHA256` to the SHA-256 of the `codex-resources/bwrap` shipped in the
|
|
28
|
+
matching upstream npm package before compiling.
|
|
29
|
+
|
|
30
|
+
After building, run:
|
|
17
31
|
|
|
18
32
|
```bash
|
|
19
|
-
|
|
33
|
+
bash pack.sh darwin-arm64 /path/to/build/bin /path/to/codex-checkout /path/to/output
|
|
20
34
|
```
|
|
35
|
+
|
|
36
|
+
Use the directory containing both `codex` and `codex-code-mode-host` (with `.exe` on
|
|
37
|
+
Windows). The script adds the matching upstream sandbox and search helpers. Build and
|
|
38
|
+
pack `darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `win32-arm64`, and
|
|
39
|
+
`win32-x64` for a native release. The installer pins their version in
|
|
40
|
+
`bin/codex-native.mjs`. When that pin changes, publish and verify the native packages
|
|
41
|
+
before publishing the version of `@webinson/peer` that uses them.
|
|
42
|
+
|
|
43
|
+
From the repository root, run `npm test --prefix cli`. The band, switches and panels have no
|
|
44
|
+
automated check yet; try them by hand in `peer-codex`.
|
|
@@ -39,20 +39,34 @@ if [[ -n ${PEER_CODEX_TOOLCHAIN:-} ]]; then
|
|
|
39
39
|
fi
|
|
40
40
|
cd -- "$checkout/codex-rs"
|
|
41
41
|
# The pinned Codex release ships its V8 build on the Codex release, not on denoland's.
|
|
42
|
-
|
|
42
|
+
target=${PEER_CODEX_BUILD_TARGET:-$(rustc ${cargo_args[@]+"${cargo_args[@]}"} -vV | awk '/^host:/ {print $2}')}
|
|
43
|
+
profile=${PEER_CODEX_BUILD_PROFILE:-dev}
|
|
44
|
+
build_args=(--profile "$profile")
|
|
45
|
+
cargo_command=("${PEER_CODEX_CARGO_COMMAND:-build}")
|
|
46
|
+
if [[ "${cargo_command[0]}" == xwin ]]; then
|
|
47
|
+
cargo_command+=(build)
|
|
48
|
+
fi
|
|
49
|
+
target_dir=${CARGO_TARGET_DIR:-"$checkout/codex-rs/target"}
|
|
50
|
+
output="$target_dir"
|
|
51
|
+
if [[ -n ${PEER_CODEX_BUILD_TARGET:-} ]]; then
|
|
52
|
+
build_args+=(--target "$target")
|
|
53
|
+
output="$output/$target"
|
|
54
|
+
fi
|
|
43
55
|
v8=152.2.0
|
|
44
|
-
v8_dir="$checkout/target/peer-v8/$
|
|
56
|
+
v8_dir="$checkout/target/peer-v8/$target"
|
|
45
57
|
v8_base="https://github.com/openai/codex/releases/download/rusty-v8-v$v8"
|
|
46
|
-
binding="src_binding_ptrcomp_sandbox_release_${
|
|
47
|
-
archive="librusty_v8_ptrcomp_sandbox_release_${
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
binding="src_binding_ptrcomp_sandbox_release_${target}.rs"
|
|
59
|
+
archive="librusty_v8_ptrcomp_sandbox_release_${target}.a.gz"
|
|
60
|
+
exe=""
|
|
61
|
+
if [[ "$target" == *-pc-windows-msvc ]]; then
|
|
62
|
+
archive="rusty_v8_ptrcomp_sandbox_release_${target}.lib.gz"
|
|
63
|
+
exe=".exe"
|
|
50
64
|
fi
|
|
51
65
|
mkdir -p -- "$v8_dir"
|
|
52
66
|
for artifact in "$archive" "$binding"; do
|
|
53
67
|
checksum=$(awk -v file="$artifact" '$2 == file {print $1}' "$checkout/third_party/v8/rusty_v8_${v8//./_}.sha256")
|
|
54
68
|
if [[ -z "$checksum" ]]; then
|
|
55
|
-
printf 'No pinned Codex V8 artifact for %s.\n' "$
|
|
69
|
+
printf 'No pinned Codex V8 artifact for %s.\n' "$target" >&2
|
|
56
70
|
exit 1
|
|
57
71
|
fi
|
|
58
72
|
if ! (cd -- "$v8_dir" && printf '%s %s\n' "$checksum" "$artifact" | shasum -a 256 -c - >/dev/null 2>&1); then
|
|
@@ -62,6 +76,7 @@ for artifact in "$archive" "$binding"; do
|
|
|
62
76
|
done
|
|
63
77
|
# The upstream release tag changes workspace versions without regenerating Cargo.lock.
|
|
64
78
|
RUSTY_V8_ARCHIVE="$v8_dir/$archive" RUSTY_V8_SRC_BINDING_PATH="$v8_dir/$binding" \
|
|
65
|
-
|
|
66
|
-
--target-dir "$
|
|
67
|
-
|
|
79
|
+
CARGO_PROFILE_DEV_DEBUG=0 cargo ${cargo_args[@]+"${cargo_args[@]}"} "${cargo_command[@]}" \
|
|
80
|
+
"${build_args[@]}" --target-dir "$target_dir" -p codex-cli -p codex-code-mode-host --bins
|
|
81
|
+
[[ "$profile" != dev ]] || profile=debug
|
|
82
|
+
printf 'Run with: PEER_CODEX_CLIENT=%q peer-codex\n' "$output/$profile/codex$exe"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
5
|
+
cli=$(cd -- "$here/../../.." && pwd)
|
|
6
|
+
platform=${1:?usage: pack.sh PLATFORM BUILD_BIN_DIR CODEX_CHECKOUT OUTPUT_DIR}
|
|
7
|
+
build=$(cd -- "${2:?missing build bin directory}" && pwd)
|
|
8
|
+
source=$(cd -- "${3:?missing pinned Codex checkout}" && pwd)
|
|
9
|
+
mkdir -p -- "${4:?missing output directory}"
|
|
10
|
+
output=$(cd -- "$4" && pwd)
|
|
11
|
+
target=$(node --input-type=module - "$cli" "$platform" <<'JS'
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
const { codexTargets } = await import(pathToFileURL(path.join(process.argv[2], "bin/codex-native.mjs")));
|
|
15
|
+
const target = codexTargets[process.argv[3]];
|
|
16
|
+
if (!target) throw new Error("Unsupported Peer Codex platform");
|
|
17
|
+
console.log(target);
|
|
18
|
+
JS
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
commit=f79b26ccbb3116b052544666a181012fb45dc49b
|
|
22
|
+
codex_version=0.166.0-alpha.5.1
|
|
23
|
+
[[ $(git -C "$source" rev-parse HEAD) == "$commit" ]]
|
|
24
|
+
stage=$(mktemp -d "${TMPDIR:-/tmp}/peer-codex-pack.XXXXXXXX")
|
|
25
|
+
trap 'rm -rf -- "$stage"' EXIT
|
|
26
|
+
archive=$(npm pack "@openai/codex@$codex_version-$platform" --ignore-scripts --pack-destination "$stage" --silent)
|
|
27
|
+
suffix=""
|
|
28
|
+
resources=()
|
|
29
|
+
case "$platform" in
|
|
30
|
+
linux-*) resources+=(codex-resources/bwrap) ;;
|
|
31
|
+
win32-*)
|
|
32
|
+
suffix=.exe
|
|
33
|
+
resources+=(codex-resources/codex-command-runner.exe codex-resources/codex-windows-sandbox-setup.exe)
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
vendor="package/vendor/$target"
|
|
37
|
+
assets=("$vendor/codex-package.json" "$vendor/codex-path/rg$suffix")
|
|
38
|
+
for asset in ${resources[@]+"${resources[@]}"}; do assets+=("$vendor/$asset"); done
|
|
39
|
+
tar -xf "$stage/$archive" -C "$stage" "${assets[@]}"
|
|
40
|
+
mkdir -p "$stage/$vendor/bin"
|
|
41
|
+
for binary in codex codex-code-mode-host; do
|
|
42
|
+
install -m 755 "$build/$binary$suffix" "$stage/$vendor/bin/$binary$suffix"
|
|
43
|
+
done
|
|
44
|
+
cp "$here/CODEX-LICENSE" "$stage/package/LICENSE"
|
|
45
|
+
cp "$here/CODEX-NOTICE" "$stage/package/NOTICE"
|
|
46
|
+
curl -fsSL --retry 2 'https://raw.githubusercontent.com/BurntSushi/ripgrep/15.2.0/LICENSE-MIT' \
|
|
47
|
+
-o "$stage/package/RIPGREP-LICENSE"
|
|
48
|
+
if [[ "$platform" == linux-* ]]; then
|
|
49
|
+
cp "$source/codex-rs/vendor/bubblewrap/LICENSE" "$stage/package/BUBBLEWRAP-LICENSE"
|
|
50
|
+
fi
|
|
51
|
+
cat >> "$stage/package/NOTICE" <<EOF
|
|
52
|
+
|
|
53
|
+
Peer Codex modifies the terminal UI, thread sharing and memory policy.
|
|
54
|
+
Upstream source: https://github.com/openai/codex/tree/$commit
|
|
55
|
+
The matching @webinson/peer npm package contains the Peer changes.
|
|
56
|
+
|
|
57
|
+
The bundled ripgrep is from https://github.com/BurntSushi/ripgrep/tree/15.2.0.
|
|
58
|
+
Linux bundles include the sandbox helper built from the Codex source above
|
|
59
|
+
under codex-rs/vendor/bubblewrap. The corresponding sources and build scripts
|
|
60
|
+
are in that same tree.
|
|
61
|
+
EOF
|
|
62
|
+
node --input-type=module - "$cli" "$platform" "$stage/package" <<'JS'
|
|
63
|
+
import fs from "node:fs";
|
|
64
|
+
import path from "node:path";
|
|
65
|
+
import { pathToFileURL } from "node:url";
|
|
66
|
+
const [cli, platform, stage] = process.argv.slice(2);
|
|
67
|
+
const [os, cpu] = platform.split("-");
|
|
68
|
+
const { codexPackageName, codexNativeVersion } = await import(pathToFileURL(path.join(cli, "bin/codex-native.mjs")));
|
|
69
|
+
const pkg = {
|
|
70
|
+
name: codexPackageName(platform),
|
|
71
|
+
version: codexNativeVersion,
|
|
72
|
+
description: `Peer Codex terminal for ${os} ${cpu}. Installed by @webinson/peer.`,
|
|
73
|
+
license: "Apache-2.0",
|
|
74
|
+
os: [os],
|
|
75
|
+
cpu: [cpu],
|
|
76
|
+
files: ["vendor", "NOTICE", "*-LICENSE"],
|
|
77
|
+
};
|
|
78
|
+
fs.writeFileSync(path.join(stage, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
79
|
+
JS
|
|
80
|
+
npm pack "$stage/package" --pack-destination "$output" --ignore-scripts
|
package/mod/codex/client/run.sh
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if [[ -z "$client" && -f "$peer_root/codex-cli.json" ]]; then
|
|
7
|
-
client=$(node -e '
|
|
8
|
-
try {
|
|
9
|
-
const data = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"));
|
|
10
|
-
if (typeof data.client === "string" && require("node:path").isAbsolute(data.client)) {
|
|
11
|
-
process.stdout.write(data.client);
|
|
12
|
-
}
|
|
13
|
-
} catch {}
|
|
14
|
-
' "$peer_root/codex-cli.json")
|
|
15
|
-
fi
|
|
16
|
-
client=${client:-"$peer_root/codex-client/codex-rs/target/debug/codex"}
|
|
17
|
-
if [[ ! -x "$client" ]]; then
|
|
18
|
-
printf 'Set up the Peer Codex terminal with peer install --codex --client.\n' >&2
|
|
19
|
-
exit 1
|
|
20
|
-
fi
|
|
21
|
-
# An existing stock daemon would use the stock memory policy.
|
|
22
|
-
PEER_CODEX_THREAD_MARK=1 exec "$client" --no-daemon "$@"
|
|
4
|
+
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
5
|
+
exec node "$here/../../../bin/peer-codex.mjs" "$@"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peer-thread-mark",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.24",
|
|
4
4
|
"description": "Marks a Claude Code thread shared with Peer (/peer) with a band above the prompt, with /peer stop and /peer start; panes for /peer-list, /peer-thread and /peer-all (search your memories)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"types": "./types/index.d.ts"
|
|
@@ -212,7 +212,11 @@ async function refresh($: EngineInterface) {
|
|
|
212
212
|
// Which memory a thread works with. Elephant (shared) adds OpenViking, its recall and its saving, to Claude Code's own
|
|
213
213
|
// memory; Inactive keeps Claude Code's own memory alone; Jellyfish (shared) keeps none at all.
|
|
214
214
|
type MemoryMode = 'openviking' | 'own' | 'none'
|
|
215
|
+
// The turn of a /peer with no message (or only a mode) only confirms the sharing, so it carries no memory at all:
|
|
216
|
+
// the person picks the thread's mode after it, and a Jellyfish picked then finds nothing in the thread already.
|
|
217
|
+
let confirming = false
|
|
215
218
|
async function memoryUse($: EngineInterface): Promise<MemoryMode> {
|
|
219
|
+
if (confirming) return 'none'
|
|
216
220
|
if (!(await read($, shared))?.isShared) return 'own'
|
|
217
221
|
return (await read($, mode)) === 'elephant' ? 'openviking' : 'none'
|
|
218
222
|
}
|
|
@@ -272,6 +276,9 @@ async function applyMemory($: EngineInterface) {
|
|
|
272
276
|
// What OpenViking's recall and Peer's team memory add to a prompt, left out where the thread keeps OpenViking off.
|
|
273
277
|
const OPENVIKING_BLOCK = /<openviking-context|<teammates-project-knowledge/
|
|
274
278
|
const OPENVIKING_TOOL = 'mcp__plugin_openviking-memory_'
|
|
279
|
+
// Claude Code's own memory files, in a tool's input however it is written: a path, a glob, a shell command that
|
|
280
|
+
// changes into ~/.claude first or looks for MEMORY.md there. A call that names both is held back.
|
|
281
|
+
const usesMemoryFiles = (input: string) => /\.claude(?=[\\/"'\s]|$)/.test(input) && /memory/i.test(input)
|
|
275
282
|
|
|
276
283
|
// The memory badge's color: an explicit value, as a surface may not know a theme key; it reads on light and dark.
|
|
277
284
|
const SAVED_COLOR = '#8b5cf6'
|
|
@@ -576,6 +583,15 @@ export const register: Register = on => {
|
|
|
576
583
|
|
|
577
584
|
// The CLI's prompt hook writes or removes the marker while a typed command goes in, so look after it.
|
|
578
585
|
on('prompt.submit', async ($, e, next) => {
|
|
586
|
+
// `/peer`, `/peer jellyfish` or `/peer elephant`: a confirming turn, with the mode it names for the turns after.
|
|
587
|
+
const peer = e.command && /^(?:.+:)?peer$/.test(e.command.name) ? e.command.args.trim().toLowerCase() : undefined
|
|
588
|
+
const wasConfirming = confirming
|
|
589
|
+
confirming = peer === '' || peer === 'jellyfish' || peer === 'elephant'
|
|
590
|
+
if (peer === 'jellyfish' || peer === 'elephant') await update($, mode, () => peer)
|
|
591
|
+
if (confirming !== wasConfirming) {
|
|
592
|
+
$.ui.invalidate('prompt.section')
|
|
593
|
+
$.ui.invalidate('prompt.context')
|
|
594
|
+
}
|
|
579
595
|
const result = await next(e)
|
|
580
596
|
await update($, collideOff, () => false)
|
|
581
597
|
await refresh($).catch(() => {})
|
|
@@ -671,6 +687,12 @@ export const register: Register = on => {
|
|
|
671
687
|
})
|
|
672
688
|
|
|
673
689
|
on('turn.complete', async ($, e, next) => {
|
|
690
|
+
// The confirming turn is over: from here on the thread's mode decides, so a press on the band reads it.
|
|
691
|
+
if (confirming) {
|
|
692
|
+
confirming = false
|
|
693
|
+
$.ui.invalidate('prompt.section')
|
|
694
|
+
$.ui.invalidate('prompt.context')
|
|
695
|
+
}
|
|
674
696
|
await refresh($).catch(() => {})
|
|
675
697
|
// OpenViking saves a thread's memories when a turn ends past its threshold, so the open pane and the Saved lines
|
|
676
698
|
// under the thread's messages look again.
|
|
@@ -692,6 +714,11 @@ export const register: Register = on => {
|
|
|
692
714
|
if (e.tool.startsWith(OPENVIKING_TOOL) && (await memoryUse($)) !== 'openviking') {
|
|
693
715
|
return { deny: 'PeerMemory is off in this thread. Use Elephant turns it on.' }
|
|
694
716
|
}
|
|
717
|
+
// Jellyfish keeps Claude Code's memory files out of the context, and an agent that knows where they live
|
|
718
|
+
// (~/.claude/projects/<project>/memory) does not read, search or write them with a tool either.
|
|
719
|
+
if (usesMemoryFiles(JSON.stringify(e)) && (await memoryUse($)) === 'none') {
|
|
720
|
+
return { deny: "Jellyfish: this thread uses no memory, Claude Code's memory files included. Use Elephant or Inactive brings them back." }
|
|
721
|
+
}
|
|
695
722
|
return next(e)
|
|
696
723
|
})
|
|
697
724
|
|
|
@@ -77,12 +77,24 @@ test('Elephant turns OpenViking on, Inactive keeps Claude Code memory alone, Jel
|
|
|
77
77
|
await search()
|
|
78
78
|
expect(toolRuns).toBe(1)
|
|
79
79
|
expect((await $.prompt.section({ name: 'memory', text: 'how memory works' })).text).toBeNull()
|
|
80
|
+
// Nor its memory files, read, searched or changed with a tool; other files under ~/.claude still are.
|
|
81
|
+
const denied = async (input: object) => {
|
|
82
|
+
const before = toolRuns
|
|
83
|
+
const result = (await $.tool.call(input as never)) as { deny?: string }
|
|
84
|
+
return toolRuns === before && Boolean(result?.deny)
|
|
85
|
+
}
|
|
86
|
+
expect(await denied({ tool: 'Read', file_path: '/home/.claude/projects/p/memory/peer-proof.md' })).toBe(true)
|
|
87
|
+
expect(await denied({ tool: 'Bash', command: 'cd ~/.claude && cat projects/p/memory/MEMORY.md' })).toBe(true)
|
|
88
|
+
expect(await denied({ tool: 'Grep', pattern: '12/13', path: '/home/.claude/projects' , glob: '**/memory/*.md' })).toBe(true)
|
|
89
|
+
expect(await denied({ tool: 'Read', file_path: '/home/.claude/settings.json' })).toBe(false)
|
|
90
|
+
expect(await denied({ tool: 'Read', file_path: '/repo/.claude-plugin/memory.json' })).toBe(false)
|
|
80
91
|
const kept = (await context()).blocks.find(b => b.name === 'claudeMd')?.text ?? ''
|
|
81
92
|
expect(kept).toContain('Be brief.')
|
|
82
93
|
expect(kept).not.toContain('a fact')
|
|
83
94
|
|
|
84
95
|
// Pressing the lit Jellyfish stops sharing: Inactive, OpenViking still off, Claude Code's own memory back.
|
|
85
96
|
await band.press({ key: 'jellyfish' } as never)
|
|
97
|
+
expect(await denied({ tool: 'Read', file_path: '/home/.claude/projects/p/memory/peer-proof.md' })).toBe(false)
|
|
86
98
|
expect(env.OPENVIKING_MEMORY_ENABLED).toBe('0')
|
|
87
99
|
expect(await recalled()).toEqual(['kept'])
|
|
88
100
|
expect((await $.prompt.section({ name: 'memory', text: 'how memory works' })).text).toBe('how memory works')
|
|
@@ -97,3 +109,74 @@ test('Elephant turns OpenViking on, Inactive keeps Claude Code memory alone, Jel
|
|
|
97
109
|
expect(cursorMoves()[0]?.slice(-3)).toEqual(['s1', '/t/s1.jsonl', '/repo'])
|
|
98
110
|
await band.unmount()
|
|
99
111
|
})
|
|
112
|
+
|
|
113
|
+
test('a bare /peer turn carries no memory; the mode picked after it decides the turns that follow', async ($, on) => {
|
|
114
|
+
let shared = false
|
|
115
|
+
on('env.get', () => ({ value: '/home' }) as never)
|
|
116
|
+
on('fs.read', (_, e) => {
|
|
117
|
+
const path = String((e as { path?: string }).path ?? '')
|
|
118
|
+
if (path.endsWith('settings.json')) return { value: JSON.stringify({ hooks: { UserPromptSubmit: [{ hooks: [{ command: '"/n/node" "/p/peer.mjs" hook prompt # peer-hook' }] }] } }) } as never
|
|
119
|
+
if (path.endsWith('/sessions/s1') && shared) return { value: JSON.stringify({ workspace: 'hotovo' }) } as never
|
|
120
|
+
throw new Error('none')
|
|
121
|
+
})
|
|
122
|
+
on('process.run', () => ({ value: { stdout: JSON.stringify({ connected: true, saves: [] }), stderr: '', exitCode: 0 } }) as never)
|
|
123
|
+
on('env.set', () => ({ value: {} }) as never)
|
|
124
|
+
on('session.id', () => ({ value: 's1' }) as never)
|
|
125
|
+
on('session.cwd', () => ({ value: '/repo' }) as never)
|
|
126
|
+
on('clock.now', () => ({ value: 1 }) as never)
|
|
127
|
+
on('command.register', (_, e) => ({ value: { command: (e as { name: string }).name } }) as never)
|
|
128
|
+
on('ui.status', () => ({ value: undefined }) as never)
|
|
129
|
+
on('ui.invalidate', () => ({ value: undefined }) as never)
|
|
130
|
+
on('ui.toast', () => ({ value: undefined }) as never)
|
|
131
|
+
on('session.start', (_, e) => ({ cwd: e.cwd }))
|
|
132
|
+
// The CLI's hook writes the marker while /peer goes in.
|
|
133
|
+
on('prompt.submit', (_, e) => {
|
|
134
|
+
if ((e as { command?: { name: string } }).command?.name === 'peer') shared = true
|
|
135
|
+
return { text: (e as { text: string }).text } as never
|
|
136
|
+
})
|
|
137
|
+
on('turn.complete', (_, e) => ({ text: (e as { answer: string }).answer }) as never)
|
|
138
|
+
on('prompt.section', () => ({ text: 'how memory works' }) as never)
|
|
139
|
+
on('prompt.context', (_, e) => ({ blocks: (e as { blocks: unknown[] }).blocks }) as never)
|
|
140
|
+
|
|
141
|
+
const memory = async () => {
|
|
142
|
+
const section = (await $.prompt.section({ name: 'memory', text: 'how memory works' })).text
|
|
143
|
+
const context = await $.prompt.context({
|
|
144
|
+
blocks: [{ name: 'claudeMd', text: CLAUDE_MD }],
|
|
145
|
+
instructionFiles: [{ path: MEMORY_FILE, kind: 'memory', content: '- a fact' }],
|
|
146
|
+
})
|
|
147
|
+
const md = context.blocks.find(b => b.name === 'claudeMd')?.text ?? ''
|
|
148
|
+
expect(md).toContain('Be brief.')
|
|
149
|
+
return { section, fact: md.includes('a fact') }
|
|
150
|
+
}
|
|
151
|
+
const endTurn = () => $.turn.complete({ answer: 'ok', durationMs: 1, isAborted: false, turnId: 't', reason: 'end_turn' } as never)
|
|
152
|
+
|
|
153
|
+
await $.session.start({ cwd: '/', surface: 'desktop', isInteractive: true })
|
|
154
|
+
expect(await memory()).toEqual({ section: 'how memory works', fact: true })
|
|
155
|
+
|
|
156
|
+
// /peer alone, the thread's first message: no memory in that turn, though Elephant is the default.
|
|
157
|
+
await $.prompt.submit({ text: '/peer', command: { name: 'peer', args: '' } } as never)
|
|
158
|
+
expect(await memory()).toEqual({ section: null, fact: false })
|
|
159
|
+
await endTurn()
|
|
160
|
+
expect(await memory()).toEqual({ section: 'how memory works', fact: true })
|
|
161
|
+
|
|
162
|
+
// Use Jellyfish after it: nothing again, so the thread never held any.
|
|
163
|
+
const band = await $.ui.mount({ ...BAND, surface: 'desktop' })
|
|
164
|
+
await band.press({ key: 'jellyfish' } as never)
|
|
165
|
+
await $.prompt.submit({ text: 'work' } as never)
|
|
166
|
+
expect(await memory()).toEqual({ section: null, fact: false })
|
|
167
|
+
await endTurn()
|
|
168
|
+
await band.unmount()
|
|
169
|
+
|
|
170
|
+
// /peer elephant names the mode itself: its own turn is still bare, the next one has memory.
|
|
171
|
+
await $.prompt.submit({ text: '/peer elephant', command: { name: 'peer', args: 'elephant' } } as never)
|
|
172
|
+
expect(await memory()).toEqual({ section: null, fact: false })
|
|
173
|
+
await endTurn()
|
|
174
|
+
await $.prompt.submit({ text: 'work' } as never)
|
|
175
|
+
expect(await memory()).toEqual({ section: 'how memory works', fact: true })
|
|
176
|
+
|
|
177
|
+
// /peer with a message is work: the mode decides its memory.
|
|
178
|
+
await $.prompt.submit({ text: '/peer jellyfish', command: { name: 'peer', args: 'jellyfish' } } as never)
|
|
179
|
+
await endTurn()
|
|
180
|
+
await $.prompt.submit({ text: '/peer fix it', command: { name: 'peer', args: 'fix it' } } as never)
|
|
181
|
+
expect(await memory()).toEqual({ section: null, fact: false })
|
|
182
|
+
})
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webinson/peer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.24",
|
|
4
4
|
"description": "Share chosen Claude Code and Codex threads with your team: who has agents running and on what.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"peer": "bin/peer.mjs",
|
|
9
|
-
"peer-codex": "
|
|
9
|
+
"peer-codex": "bin/peer-codex.mjs"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|