@webinson/peer 0.4.16 → 0.4.18
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 +79 -2
- package/bin/codex-thread-mark.mjs +232 -0
- package/bin/peer.mjs +208 -46
- package/mod/codex/.agents/plugins/marketplace.json +12 -0
- package/mod/codex/client/.gitattributes +1 -0
- package/mod/codex/client/0001-memory.patch +327 -0
- package/mod/codex/client/0002-terminal.patch +308 -0
- package/mod/codex/client/CODEX-LICENSE +201 -0
- package/mod/codex/client/CODEX-NOTICE +6 -0
- package/mod/codex/client/README.md +20 -0
- package/mod/codex/client/build.sh +67 -0
- package/mod/codex/client/run.sh +22 -0
- package/mod/codex/client/src/actions.rs +217 -0
- package/mod/codex/client/src/band.rs +469 -0
- package/mod/codex/client/src/openviking.rs +110 -0
- package/mod/codex/client/src/policy.rs +132 -0
- package/mod/codex/peer-thread-mark/.codex-plugin/plugin.json +12 -0
- package/mod/codex/peer-thread-mark/hooks/hooks.json +9 -0
- package/mod/codex/peer-thread-mark/hooks/peer.mjs +17 -0
- package/mod/peer-thread-mark/.claude-plugin/plugin.json +1 -1
- package/mod/peer-thread-mark/hooks/register.tsx +149 -1
- package/mod/peer-thread-mark/tests/collision.test.tsx +150 -0
- package/mod/peer-thread-mark/types/index.d.ts +14 -1
- package/package.json +4 -3
package/bin/peer.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Peer: shows a team which agent threads its people chose to share, and on what they work.
|
|
3
3
|
// `peer login` signs in once; `/peer` in a Claude Code thread shares that one thread.
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
+
import crypto from "node:crypto";
|
|
5
6
|
import fs from "node:fs";
|
|
6
7
|
import os from "node:os";
|
|
7
8
|
import path from "node:path";
|
|
@@ -30,11 +31,12 @@ const MARKETPLACE = "webinson-peer";
|
|
|
30
31
|
|
|
31
32
|
const HELP = `peer: share chosen agent threads with your team
|
|
32
33
|
|
|
33
|
-
peer login [email] [--hub <url>] [--workspace <slug>]
|
|
34
|
-
sign in once and set up Claude Code
|
|
34
|
+
peer login [email] [--hub <url>] [--workspace <slug>] [--codex [--client]]
|
|
35
|
+
sign in once and set up Claude Code or Codex
|
|
35
36
|
peer list people in the workspace, their shared threads and what they work on
|
|
36
37
|
peer status who you are, the workspace and the threads this computer shares
|
|
37
|
-
peer doctor why Peer's hook did not answer /peer
|
|
38
|
+
peer doctor why Peer's hook did not answer /peer, and whether this repository is in one
|
|
39
|
+
Hub project (edits are checked against teammates' there)
|
|
38
40
|
peer team connect <url> --as <member> [--project <id>]
|
|
39
41
|
connect to the team's memory server (your key on stdin); new threads in the
|
|
40
42
|
project's repositories then start with teammates' published findings. The
|
|
@@ -43,14 +45,15 @@ const HELP = `peer: share chosen agent threads with your team
|
|
|
43
45
|
peer all [words] [here] [--newest | --oldest] [page N]
|
|
44
46
|
your own memories in your personal OpenViking, or the ones with these words
|
|
45
47
|
(full text); here: only this repository's. /peer-all in Claude Code
|
|
46
|
-
peer install
|
|
47
|
-
|
|
48
|
+
peer install [--codex [--client]]
|
|
49
|
+
set up the thread mark; --client also builds the Peer Codex terminal
|
|
50
|
+
peer uninstall [--codex] remove the integration from Claude Code or Codex
|
|
48
51
|
peer logout forget the sign-in on this computer
|
|
49
52
|
|
|
50
53
|
In Claude Code: /peer shares the thread (/peer <message> also sends the message on), /peer stop
|
|
51
54
|
ends it, /peer-list shows the list.
|
|
52
|
-
Only threads where /peer was typed are shared, and only their repository, branch and
|
|
53
|
-
never prompts, answers or
|
|
55
|
+
Only threads where /peer was typed are shared, and only their repository, branch, state and the
|
|
56
|
+
paths of the files they edit: never prompts, answers, summaries or file contents.`;
|
|
54
57
|
|
|
55
58
|
// ---- files ----
|
|
56
59
|
|
|
@@ -133,8 +136,8 @@ function workingOn(cwd) {
|
|
|
133
136
|
return { repository, branch: branch && branch !== "HEAD" ? branch : undefined, repoKey };
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
async function report(saved, session, state, cwd) {
|
|
137
|
-
await call(saved, "PUT", `/workspaces/${saved.workspace}/agents/${session}`, { agent
|
|
139
|
+
async function report(saved, session, state, cwd, agent = "claude-code", timeoutMs = HUB_TIMEOUT_MS) {
|
|
140
|
+
await call(saved, "PUT", `/workspaces/${saved.workspace}/agents/${session}`, { agent, state, ...workingOn(cwd) }, timeoutMs);
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
// ---- the list ----
|
|
@@ -235,6 +238,40 @@ function log(line) {
|
|
|
235
238
|
} catch {}
|
|
236
239
|
}
|
|
237
240
|
|
|
241
|
+
async function shareThread(saved, session, cwd, working, agent, mode) {
|
|
242
|
+
const sharedAt = new Date().toISOString();
|
|
243
|
+
writeJson(markerOf(session), { workspace: saved.workspace, sharedAt, mode });
|
|
244
|
+
const where = workingOn(cwd);
|
|
245
|
+
const what = [where.repository, where.branch].filter(Boolean).map(plain).join(" @ ");
|
|
246
|
+
// Claude Code's thread mark checks each edit against teammates' (`peer collide`); Codex sends no paths.
|
|
247
|
+
const edits = !agent || agent === "claude-code";
|
|
248
|
+
let said = `Peer: this thread is shared in ${saved.workspace}${what ? ` as ${what}` : ""}. Teammates see its repository, branch${edits ? ", state and the paths of the files it edits" : " and state"}, nothing of the conversation. /peer stop ends it.`;
|
|
249
|
+
// The Hub project its edits are checked in, looked up beside the report; `project` in the marker also says
|
|
250
|
+
// the thread was shared by a version that sends edited paths.
|
|
251
|
+
const [, project] = await Promise.all([
|
|
252
|
+
report(saved, session, working ? "working" : "idle", cwd, agent).catch((error) => {
|
|
253
|
+
said = `Peer: shared, but the hub did not answer yet (${error.message}). The next turn tries again.`;
|
|
254
|
+
}),
|
|
255
|
+
edits ? shareProject(saved, where.repoKey) : undefined,
|
|
256
|
+
]);
|
|
257
|
+
if (edits) {
|
|
258
|
+
writeJson(markerOf(session), { workspace: saved.workspace, sharedAt, mode, project, ...(where.repoKey ? { repoKey: where.repoKey } : {}) });
|
|
259
|
+
try {
|
|
260
|
+
environment();
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
return { said, what };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function stopSharing(saved, session, cwd, agent) {
|
|
267
|
+
const marker = markerOf(session);
|
|
268
|
+
const shared = readJson(marker);
|
|
269
|
+
fs.rmSync(marker, { force: true });
|
|
270
|
+
if (!shared?.workspace) return "Peer: this thread was not shared.";
|
|
271
|
+
await report({ ...saved, workspace: shared.workspace }, session, "stopped", cwd, agent).catch(() => {});
|
|
272
|
+
return "Peer: this thread is no longer shared.";
|
|
273
|
+
}
|
|
274
|
+
|
|
238
275
|
async function hook(event) {
|
|
239
276
|
const input = await readStdin();
|
|
240
277
|
const session = input.session_id;
|
|
@@ -268,19 +305,10 @@ async function hook(event) {
|
|
|
268
305
|
return block(`Peer: ${error.message}`);
|
|
269
306
|
}
|
|
270
307
|
if (argument === "stop") {
|
|
271
|
-
|
|
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));
|
|
308
|
+
return block(await stopSharing(saved, session, input.cwd), retitle(input, false));
|
|
275
309
|
}
|
|
276
310
|
// `/peer <message>` shares the thread and lets the message through: the command hands it to the model.
|
|
277
|
-
|
|
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;
|
|
311
|
+
const { said, what } = await shareThread(saved, session, input.cwd, Boolean(argument));
|
|
284
312
|
const sessionTitle = retitle(input, true, what);
|
|
285
313
|
// A bare /peer reaches the agent too, which confirms in one line: a block would show the person Claude
|
|
286
314
|
// Code's "hook blocked your prompt" warning for what went right.
|
|
@@ -319,11 +347,11 @@ function forgetOldThreads() {
|
|
|
319
347
|
}
|
|
320
348
|
|
|
321
349
|
// 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) {
|
|
350
|
+
async function reportQuietly(session, state, cwd, agent, timeoutMs) {
|
|
323
351
|
const saved = config();
|
|
324
352
|
const marker = readJson(markerOf(session));
|
|
325
353
|
if (!saved?.session || !marker?.workspace) return;
|
|
326
|
-
await report({ ...saved, workspace: marker.workspace }, session, state, cwd).catch(() => {});
|
|
354
|
+
await report({ ...saved, workspace: marker.workspace }, session, state, cwd, agent, timeoutMs).catch(() => {});
|
|
327
355
|
}
|
|
328
356
|
|
|
329
357
|
// ---- Claude Code setup ----
|
|
@@ -336,7 +364,7 @@ const SHARED_NOTE = "[Peer's hook ran]";
|
|
|
336
364
|
// Added when /peer came alone: there is no message to work on, only the confirmation, and its privacy line
|
|
337
365
|
// is a promise the person relies on, so it stays whole.
|
|
338
366
|
const ALONE_NOTE =
|
|
339
|
-
"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 and
|
|
367
|
+
"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.";
|
|
340
368
|
|
|
341
369
|
const COMMAND_FILES = {
|
|
342
370
|
"peer.md": `---
|
|
@@ -458,6 +486,10 @@ function option(args, name) {
|
|
|
458
486
|
}
|
|
459
487
|
|
|
460
488
|
async function login(args) {
|
|
489
|
+
const forCodex = args.includes("--codex");
|
|
490
|
+
const client = args.includes("--client");
|
|
491
|
+
if (client && !forCodex) throw new Error("--client requires --codex");
|
|
492
|
+
args = args.filter((arg) => arg !== "--codex" && arg !== "--client");
|
|
461
493
|
const hub = (option(args, "--hub") ?? process.env.PEER_HUB ?? config()?.hub ?? DEFAULT_HUB).replace(/\/+$/, "");
|
|
462
494
|
let workspace = option(args, "--workspace");
|
|
463
495
|
const ask = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -493,7 +525,8 @@ async function login(args) {
|
|
|
493
525
|
} finally {
|
|
494
526
|
ask.close();
|
|
495
527
|
}
|
|
496
|
-
|
|
528
|
+
if (forCodex) (await import("./codex-thread-mark.mjs")).installCodex(codexPeer, { client });
|
|
529
|
+
else install();
|
|
497
530
|
}
|
|
498
531
|
|
|
499
532
|
async function logout() {
|
|
@@ -589,8 +622,26 @@ async function doctor() {
|
|
|
589
622
|
}
|
|
590
623
|
}
|
|
591
624
|
|
|
592
|
-
|
|
625
|
+
// Edits are checked against teammates' only in a repository that one Hub project lists. Reads the manifest only.
|
|
626
|
+
const repoKey = repositoryPeer(process.cwd());
|
|
627
|
+
if (saved?.session && saved?.workspace && repoKey) {
|
|
628
|
+
const project = await shareProject(saved, repoKey);
|
|
629
|
+
if (project === "failed") bad(`the hub did not answer which project holds ${repoKey}: run \`peer doctor\` again`);
|
|
630
|
+
else if (project === "none") bad(`no single Hub project in ${saved.workspace} lists ${repoKey}, so edits are not checked against teammates': add the repository to one project`);
|
|
631
|
+
else ok(`${repoKey} is in Hub project ${project}: edits in shared threads are checked against teammates'`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const all = fs.existsSync(HOOK_LOG) ? fs.readFileSync(HOOK_LOG, "utf8").trim().split("\n") : [];
|
|
635
|
+
// Edit checks come one per edit: counted by state, so they do not push the other runs out of view.
|
|
636
|
+
const isCollide = (l) => l.split(" ")[1] === "collide";
|
|
637
|
+
const lines = all.filter((l) => !isCollide(l)).slice(-5);
|
|
593
638
|
if (lines.length) console.log(`\nLast hook runs (${HOOK_LOG}):\n${lines.map((l) => ` ${l}`).join("\n")}`);
|
|
639
|
+
const collides = all.filter(isCollide);
|
|
640
|
+
if (collides.length) {
|
|
641
|
+
const states = {};
|
|
642
|
+
for (const l of collides) states[l.split(" ")[2]] = (states[l.split(" ")[2]] ?? 0) + 1;
|
|
643
|
+
console.log(`Edit checks: ${Object.entries(states).map(([state, n]) => `${n} ${state}`).join(", ")}`);
|
|
644
|
+
}
|
|
594
645
|
console.log(
|
|
595
646
|
problems
|
|
596
647
|
? `\n${problems} thing(s) to fix. Then start a new Claude Code session.`
|
|
@@ -823,19 +874,20 @@ function threadUses(session, prompts) {
|
|
|
823
874
|
return uses.slice(-200);
|
|
824
875
|
}
|
|
825
876
|
|
|
826
|
-
async function threadMemories(session) {
|
|
877
|
+
async function threadMemories(session, codex = false) {
|
|
827
878
|
const ov = readJson(OV_CONF);
|
|
828
879
|
if (!ov?.url) return { connected: false, reason: `no OpenViking client config at ${OV_CONF}` };
|
|
829
|
-
const base = `viking://user/${ov.user}/sessions
|
|
880
|
+
const base = `viking://user/${ov.user}/sessions/${codex ? "cx" : "cc"}-${session}`;
|
|
881
|
+
const uses = (prompts) => codex ? [] : threadUses(session, prompts);
|
|
830
882
|
let archives;
|
|
831
883
|
try {
|
|
832
884
|
archives = await ovRead(ov, `/fs/ls?uri=${encodeURIComponent(`${base}/history`)}`);
|
|
833
885
|
} catch (error) {
|
|
834
|
-
if (/not found|no such|404/i.test(error.message)) return { connected: true, saves: [], uses:
|
|
886
|
+
if (/not found|no such|404/i.test(error.message)) return { connected: true, saves: [], uses: uses(codex ? [] : transcriptPrompts(session)) };
|
|
835
887
|
throw error;
|
|
836
888
|
}
|
|
837
889
|
const saves = [];
|
|
838
|
-
const prompts = transcriptPrompts(session);
|
|
890
|
+
const prompts = codex ? [] : transcriptPrompts(session);
|
|
839
891
|
for (const archive of archives.filter((entry) => entry.isDir)) {
|
|
840
892
|
let diff;
|
|
841
893
|
try {
|
|
@@ -872,7 +924,15 @@ async function threadMemories(session) {
|
|
|
872
924
|
items.forEach((item, index) => (item.body = String(bodies[index] ?? "").slice(0, 3000)));
|
|
873
925
|
saves.push({ at: diff.extracted_at ?? archive.modTime, items });
|
|
874
926
|
}
|
|
875
|
-
return { connected: true, saves: saves.sort((a, b) => String(b.at).localeCompare(String(a.at))), uses:
|
|
927
|
+
return { connected: true, saves: saves.sort((a, b) => String(b.at).localeCompare(String(a.at))), uses: uses(prompts) };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function threadMemoryText(memories) {
|
|
931
|
+
if (!memories.connected) return `Peer: ${memories.reason}`;
|
|
932
|
+
if (!memories.saves.length) return "This thread has not saved anything to memory yet.";
|
|
933
|
+
return memories.saves.flatMap((save) => [save.at, ...save.items.map((item) =>
|
|
934
|
+
` ${item.change} ${item.type} "${item.name}": ${item.lines.join(" / ")}${item.systemText ? " [holds Claude Code's own text]" : ""}`,
|
|
935
|
+
)]).join("\n");
|
|
876
936
|
}
|
|
877
937
|
|
|
878
938
|
// ---- team memory: teammates' published project knowledge at the start of a thread ----
|
|
@@ -912,13 +972,14 @@ async function teamCall(team, method, route, body, timeoutMs = 3000) {
|
|
|
912
972
|
return { status: response.status, value: await response.json().catch(() => ({})) };
|
|
913
973
|
}
|
|
914
974
|
|
|
975
|
+
const keysOf = (project) => (project.repositories ?? []).map((r) => repositoryKey(r.url ?? "")).filter(Boolean);
|
|
976
|
+
|
|
915
977
|
// The project is the Hub's: the workspace project whose repositories include this one, with all of its
|
|
916
978
|
// repositories. A repository in no Hub project (or without a sign-in) is a project of its own, named by its key.
|
|
917
979
|
async function teamProject(repo) {
|
|
918
980
|
const saved = config();
|
|
919
981
|
if (!saved?.session || !saved?.workspace) return { project: repo, repos: [repo] };
|
|
920
982
|
const manifest = await call(saved, "GET", `/workspaces/${saved.workspace}/manifest`, undefined, 10000);
|
|
921
|
-
const keysOf = (project) => (project.repositories ?? []).map((r) => repositoryKey(r.url ?? "")).filter(Boolean);
|
|
922
983
|
const found = (manifest?.projects ?? []).find((project) => keysOf(project).includes(repo));
|
|
923
984
|
return found ? { project: found.id, repos: keysOf(found) } : { project: repo, repos: [repo] };
|
|
924
985
|
}
|
|
@@ -973,8 +1034,7 @@ function changesSince(cwd, paths, date) {
|
|
|
973
1034
|
return log ? log.split("\n").filter(Boolean).length : 0;
|
|
974
1035
|
}
|
|
975
1036
|
|
|
976
|
-
async function teamHook(event) {
|
|
977
|
-
const input = await readStdin();
|
|
1037
|
+
async function teamHook(event, input) {
|
|
978
1038
|
const team = readJson(TEAM_FILE);
|
|
979
1039
|
const session = input.session_id;
|
|
980
1040
|
if (!team || !validSession(session)) return;
|
|
@@ -1033,7 +1093,7 @@ async function teamHook(event) {
|
|
|
1033
1093
|
...entries,
|
|
1034
1094
|
"</teammates-project-knowledge>",
|
|
1035
1095
|
].join("\n");
|
|
1036
|
-
|
|
1096
|
+
return { hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: block } };
|
|
1037
1097
|
}
|
|
1038
1098
|
|
|
1039
1099
|
// ---- your own memory: what your personal OpenViking remembered, listed and searched ----
|
|
@@ -1191,6 +1251,91 @@ async function memoryText(query) {
|
|
|
1191
1251
|
|
|
1192
1252
|
// ---- main ----
|
|
1193
1253
|
|
|
1254
|
+
// ---- the collision signal: after an edit in a shared thread, who else was on that file ----
|
|
1255
|
+
// The thread-mark mod runs `peer collide` after each edit; the Hub's `intent` records the claim and answers whose
|
|
1256
|
+
// other sessions are on the path (claims count 180 s). Only the path leaves the computer, never its contents.
|
|
1257
|
+
|
|
1258
|
+
const ENVIRONMENT = path.join(HOME, "environment");
|
|
1259
|
+
const COLLIDE_TIMEOUT_MS = 1500;
|
|
1260
|
+
|
|
1261
|
+
// The Hub project of a shared thread: the one project listing this repository, `none` for zero or several, `failed`
|
|
1262
|
+
// when the Hub does not answer.
|
|
1263
|
+
async function shareProject(saved, repoKey) {
|
|
1264
|
+
if (!repoKey) return "none";
|
|
1265
|
+
try {
|
|
1266
|
+
const manifest = await call(saved, "GET", `/workspaces/${saved.workspace}/manifest`, undefined, HUB_TIMEOUT_MS);
|
|
1267
|
+
const found = (manifest?.projects ?? []).filter((project) => keysOf(project).includes(repoKey));
|
|
1268
|
+
return found.length === 1 ? found[0].id : "none";
|
|
1269
|
+
} catch {
|
|
1270
|
+
return "failed";
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// This computer as the Hub's claims name it: made once, kept across sign-ins, so a claim stays this computer's.
|
|
1275
|
+
function environment() {
|
|
1276
|
+
try {
|
|
1277
|
+
return fs.readFileSync(ENVIRONMENT, "utf8").trim();
|
|
1278
|
+
} catch {}
|
|
1279
|
+
fs.mkdirSync(HOME, { recursive: true, mode: 0o700 });
|
|
1280
|
+
try {
|
|
1281
|
+
fs.writeFileSync(ENVIRONMENT, crypto.randomUUID(), { flag: "wx", mode: 0o600 });
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
if (error.code !== "EEXIST") throw error;
|
|
1284
|
+
}
|
|
1285
|
+
return fs.readFileSync(ENVIRONMENT, "utf8").trim();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// A path inside the repository as the Hub accepts it: relative, forward slashes, no `..`, no control characters.
|
|
1289
|
+
const repositoryFile = (relative) =>
|
|
1290
|
+
relative && !relative.startsWith("/") && !relative.includes(":") && !/[\u0000-\u001f\u007f]/.test(relative) && !relative.split("/").includes("..") && relative.length <= 300;
|
|
1291
|
+
|
|
1292
|
+
async function collide(session, toolUseId, file) {
|
|
1293
|
+
const started = Date.now();
|
|
1294
|
+
let relative = null;
|
|
1295
|
+
const done = (state, reason, holders = []) => {
|
|
1296
|
+
log(`collide ${state} ${reason ?? "-"} ${Date.now() - started} ms`);
|
|
1297
|
+
console.log(JSON.stringify({ path: relative, state, ...(reason ? { reason } : {}), holders }));
|
|
1298
|
+
};
|
|
1299
|
+
const saved = config();
|
|
1300
|
+
const marker = validSession(session) ? readJson(markerOf(session)) : null;
|
|
1301
|
+
if (!saved?.session || !marker?.workspace || !marker.project) return done("skipped");
|
|
1302
|
+
if (marker.project === "none") return done("no-project");
|
|
1303
|
+
if (marker.project === "failed") return done("unchecked", "lookup-failed");
|
|
1304
|
+
let real;
|
|
1305
|
+
try {
|
|
1306
|
+
real = fs.realpathSync(String(file ?? ""));
|
|
1307
|
+
} catch {
|
|
1308
|
+
return done("skipped");
|
|
1309
|
+
}
|
|
1310
|
+
const top = git(path.dirname(real), "rev-parse", "--show-toplevel");
|
|
1311
|
+
if (!top || repositoryPeer(path.dirname(real)) !== marker.repoKey) return done("skipped");
|
|
1312
|
+
relative = path.relative(fs.realpathSync(top), real).split(path.sep).join("/");
|
|
1313
|
+
if (!repositoryFile(relative)) {
|
|
1314
|
+
relative = null;
|
|
1315
|
+
return done("skipped");
|
|
1316
|
+
}
|
|
1317
|
+
let answered;
|
|
1318
|
+
try {
|
|
1319
|
+
const body = { environment: environment(), session: `cc-${session}`, paths: [relative], ...(toolUseId ? { op: String(toolUseId).slice(0, 64) } : {}) };
|
|
1320
|
+
answered = await call({ ...saved, workspace: marker.workspace }, "POST", `/workspaces/${marker.workspace}/coord/${encodeURIComponent(marker.project)}/intent`, body, COLLIDE_TIMEOUT_MS);
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
return done("unchecked", error?.name === "TimeoutError" ? "hub-timeout" : "hub-error");
|
|
1323
|
+
}
|
|
1324
|
+
const verdict = (answered?.verdicts ?? []).find((v) => v.path === relative);
|
|
1325
|
+
if (!verdict) return done("clear");
|
|
1326
|
+
if (!Array.isArray(verdict.holders)) return done("unchecked", "old-hub");
|
|
1327
|
+
const me = String(saved.email ?? "").toLowerCase();
|
|
1328
|
+
const holders = verdict.holders.map((h) => ({ email: String(h.email ?? ""), at: h.at, own: String(h.email ?? "").toLowerCase() === me }));
|
|
1329
|
+
return done(holders.length ? "collision" : "clear", undefined, holders);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const codexPeer = {
|
|
1333
|
+
home: HOME, cli: SELF, mod: MOD_DIR,
|
|
1334
|
+
validSession, readJson, writeJson, markerOf, config, forgetOldThreads,
|
|
1335
|
+
reportQuietly, signedIn, shareThread, stopSharing, listText, memoryQuery, memoryPage, memoryText,
|
|
1336
|
+
threadMemories, threadMemoryText, teamHook,
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1194
1339
|
const [command, ...args] = process.argv.slice(2);
|
|
1195
1340
|
try {
|
|
1196
1341
|
switch (command) {
|
|
@@ -1211,16 +1356,10 @@ try {
|
|
|
1211
1356
|
case "thread": {
|
|
1212
1357
|
// `peer thread --json <session id>` is for the thread-mark mod's pane.
|
|
1213
1358
|
const session = args.find((arg) => !arg.startsWith("--"));
|
|
1214
|
-
if (!validSession(session)) throw new Error("usage: peer thread [--json] <
|
|
1215
|
-
const memories = await threadMemories(session);
|
|
1359
|
+
if (!validSession(session)) throw new Error("usage: peer thread [--json] [--codex] <session id>");
|
|
1360
|
+
const memories = await threadMemories(session, args.includes("--codex"));
|
|
1216
1361
|
if (args.includes("--json")) console.log(JSON.stringify(memories));
|
|
1217
|
-
else
|
|
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
|
-
}
|
|
1362
|
+
else console.log(threadMemoryText(memories));
|
|
1224
1363
|
break;
|
|
1225
1364
|
}
|
|
1226
1365
|
case "status":
|
|
@@ -1231,14 +1370,31 @@ try {
|
|
|
1231
1370
|
break;
|
|
1232
1371
|
case "install":
|
|
1233
1372
|
signedIn();
|
|
1234
|
-
|
|
1373
|
+
if (args.includes("--codex")) {
|
|
1374
|
+
(await import("./codex-thread-mark.mjs")).installCodex(codexPeer, { client: args.includes("--client") });
|
|
1375
|
+
} else if (args.includes("--client")) throw new Error("--client requires --codex");
|
|
1376
|
+
else install();
|
|
1235
1377
|
break;
|
|
1236
1378
|
case "uninstall":
|
|
1237
|
-
|
|
1379
|
+
if (args.includes("--codex")) (await import("./codex-thread-mark.mjs")).uninstallCodex(codexPeer);
|
|
1380
|
+
else uninstall();
|
|
1381
|
+
break;
|
|
1382
|
+
case "codex-control":
|
|
1383
|
+
answer(await (await import("./codex-thread-mark.mjs")).codexControl(await readStdin(), codexPeer));
|
|
1238
1384
|
break;
|
|
1239
1385
|
case "hook":
|
|
1240
1386
|
// A hook never breaks the person's session: whatever goes wrong, it exits 0.
|
|
1241
|
-
await (
|
|
1387
|
+
await (async () => {
|
|
1388
|
+
if (args[0] === "codex") {
|
|
1389
|
+
const { codexHook } = await import("./codex-thread-mark.mjs");
|
|
1390
|
+
const output = await codexHook(await readStdin(), codexPeer);
|
|
1391
|
+
if (output) answer(output);
|
|
1392
|
+
} else if (args[0]?.startsWith("team-")) {
|
|
1393
|
+
const output = await teamHook(args[0], await readStdin());
|
|
1394
|
+
if (output) answer(output);
|
|
1395
|
+
}
|
|
1396
|
+
else await hook(args[0]);
|
|
1397
|
+
})().catch((error) => log(`hook ${args[0]} failed: ${error?.message ?? error}`));
|
|
1242
1398
|
break;
|
|
1243
1399
|
case "all": {
|
|
1244
1400
|
// `peer all --json …` is for the thread-mark mod's pane; without a personal OpenViking it says so.
|
|
@@ -1251,6 +1407,12 @@ try {
|
|
|
1251
1407
|
}
|
|
1252
1408
|
break;
|
|
1253
1409
|
}
|
|
1410
|
+
case "collide": {
|
|
1411
|
+
// `peer collide --json <session> <tool_use_id> <absolute path>` is for the thread-mark mod, after an edit.
|
|
1412
|
+
const [session, toolUseId, file] = args.filter((arg) => arg !== "--json");
|
|
1413
|
+
await collide(session, toolUseId, file);
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1254
1416
|
case "team":
|
|
1255
1417
|
if (args[0] === "connect") await teamConnect(args.slice(1));
|
|
1256
1418
|
else if (args[0] === "status") teamStatus();
|
|
@@ -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
|