@webinson/peer 0.4.30 → 0.4.32
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
|
@@ -215,7 +215,8 @@ query. Nothing else of the thread is sent. `peer team status` shows the connecti
|
|
|
215
215
|
|
|
216
216
|
In a shared thread, an edit of a file a teammate's page names in its `paths` also tells the agent about that page,
|
|
217
217
|
right after the edit: at most three pages an edit and five a thread, each once, the page about the file itself
|
|
218
|
-
before one about a folder above it, never your own page and never a hypothesis
|
|
218
|
+
before one about a folder above it, never your own page and never a hypothesis; a page that names only a
|
|
219
|
+
top-level folder (`cli/`) comes with the prompt, not after an edit. Peer writes the note's provenance
|
|
219
220
|
(whose agent wrote it, its status, the date it was last confirmed, which path it is about, "may be stale") and fences
|
|
220
221
|
the page's first lines as data. The edit asks the team server nothing: it reads a local list of the project's pages
|
|
221
222
|
by path (`~/.peer/team-memory/index-<project>.json`), which the prompt hook refreshes in the background at most every
|
|
@@ -234,10 +234,16 @@ async function collideHook(input, session, peer) {
|
|
|
234
234
|
// PEER_SESSION_TOKEN: Claude Code sessions near the app pass it on to every terminal started from them.
|
|
235
235
|
if (process.env.PEER_RUN) return;
|
|
236
236
|
if (!peer.readJson(peer.markerOf(session))) return;
|
|
237
|
-
const state = collideState(peer, session);
|
|
238
|
-
if (state.read().breaker) return;
|
|
239
237
|
const files = patchPaths(input.tool_input?.command ?? input.tool_input);
|
|
240
238
|
if (!files.length) return;
|
|
239
|
+
// A teammate's page about the files comes from the local index, so it is told even when the Hub is not asked.
|
|
240
|
+
let knowledge;
|
|
241
|
+
try {
|
|
242
|
+
knowledge = peer.teamNotesAt?.({ session, cwd: input.cwd || process.cwd(), files });
|
|
243
|
+
} catch {}
|
|
244
|
+
const notesOnly = knowledge ? { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: knowledge } } : undefined;
|
|
245
|
+
const state = collideState(peer, session);
|
|
246
|
+
if (state.read().breaker) return notesOnly;
|
|
241
247
|
const results = await peer.collidePaths({
|
|
242
248
|
session,
|
|
243
249
|
hubSession: session,
|
|
@@ -248,7 +254,7 @@ async function collideHook(input, session, peer) {
|
|
|
248
254
|
});
|
|
249
255
|
if (results.some((result) => result.reason === "hub-timeout")) {
|
|
250
256
|
state.update((now) => ({ ...now, breaker: true }));
|
|
251
|
-
return;
|
|
257
|
+
return notesOnly;
|
|
252
258
|
}
|
|
253
259
|
// A path is news when the set of other people on it differs from the set named last time.
|
|
254
260
|
const others = (result) => [...new Set((result.people ?? []).filter((person) => !person.own).map((person) => person.email.toLowerCase()))].sort().join(" ");
|
|
@@ -260,10 +266,6 @@ async function collideHook(input, session, peer) {
|
|
|
260
266
|
return { ...now, shown };
|
|
261
267
|
});
|
|
262
268
|
const sentence = peer.collisionSentence(fresh);
|
|
263
|
-
let knowledge;
|
|
264
|
-
try {
|
|
265
|
-
knowledge = peer.teamNotesAt?.({ session, cwd: input.cwd || process.cwd(), files });
|
|
266
|
-
} catch {}
|
|
267
269
|
const told = [sentence, knowledge].filter(Boolean).join("\n");
|
|
268
270
|
if (!told) return;
|
|
269
271
|
return { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: told } };
|
package/bin/peer.mjs
CHANGED
|
@@ -1139,7 +1139,8 @@ async function teamHook(event, input) {
|
|
|
1139
1139
|
const query = `${prompt.slice(0, 400)}${branch && branch !== "HEAD" ? ` (branch ${branch})` : ""}`;
|
|
1140
1140
|
const search = async (target) =>
|
|
1141
1141
|
((await teamCall(team, "POST", "/search/find", { query, target_uri: target, limit: 10 })).value.result?.resources ?? [])
|
|
1142
|
-
|
|
1142
|
+
// A folder's own summary (.abstract.md, .overview.md) is no page; an empty knowledge/ returns only that.
|
|
1143
|
+
.filter((hit) => hit.score >= 0.7 && hit.uri.endsWith(".md") && !hit.uri.split("/").pop().startsWith("."));
|
|
1143
1144
|
let hits = await search(`${root}/knowledge`);
|
|
1144
1145
|
if (!hits.length) hits = await search(`${root}/inputs`);
|
|
1145
1146
|
|
|
@@ -1197,7 +1198,7 @@ function teamPageFields(text) {
|
|
|
1197
1198
|
const [, kind, status, author] = meta;
|
|
1198
1199
|
const contributors = (text.match(/^contributors: (.+)$/m)?.[1] ?? author).split(/,\s*/).filter(Boolean);
|
|
1199
1200
|
const paths = (text.match(/^paths: (.+)$/m)?.[1] ?? "").split(/,\s*/).map((p) => p.trim()).filter((p) => p && !p.includes("..") && !p.startsWith("/"));
|
|
1200
|
-
const title =
|
|
1201
|
+
const title = String(text.match(/^# (.+)$/m)?.[1] ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/g, "").slice(0, 240);
|
|
1201
1202
|
const date = text.match(/last_confirmed: (\d{4}-\d{2}-\d{2})/)?.[1] ?? "";
|
|
1202
1203
|
const body = text.split("\n").find((line, i) => i > 2 && line.trim() && !/^(kind|paths|contributors):/.test(line) && !line.startsWith("#")) ?? "";
|
|
1203
1204
|
return { kind, status, author, contributors, paths, title, date, body: body.slice(0, 600) };
|
|
@@ -1244,8 +1245,10 @@ function refreshTeamIndex(team) {
|
|
|
1244
1245
|
} catch {}
|
|
1245
1246
|
}
|
|
1246
1247
|
|
|
1247
|
-
// A page governs a file when one of its paths is the file or a folder above it.
|
|
1248
|
-
|
|
1248
|
+
// A page governs a file when one of its paths is the file or a folder above it. A top-level folder (`cli/`) names
|
|
1249
|
+
// too much of the repository to say anything about one edit: such a page still comes with the prompt, not here.
|
|
1250
|
+
const broad = (anchor) => !anchor.replace(/\/+$/, "").includes("/") && !/\.[\w]+$/.test(anchor);
|
|
1251
|
+
const governs = (anchor, file) => !broad(anchor) && (anchor === file || file.startsWith(anchor.endsWith("/") ? anchor : `${anchor}/`));
|
|
1249
1252
|
const fenceText = (text, max = 160) => String(text ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/[<>]/g, "").trim().slice(0, max);
|
|
1250
1253
|
|
|
1251
1254
|
// The fenced note for the agent after an edit of `files` (absolute or relative to `cwd`), or undefined.
|
|
@@ -1286,7 +1289,7 @@ function teamNotesAt({ session, cwd, files }) {
|
|
|
1286
1289
|
const on = page.paths.filter((anchor) => relative.some((file) => governs(anchor, file)));
|
|
1287
1290
|
const changed = changesSince(base, page.paths, page.date);
|
|
1288
1291
|
const stale = changed >= STALE_AFTER ? `; may be stale: ${changed} commits touched its paths since ${page.date}` : "";
|
|
1289
|
-
return `- ${fenceText(page.title)} (${page.status} ${page.kind}, written by ${fenceText(by)}'s agent, last confirmed ${page.date || "unknown"}, about ${on.map((anchor) => fenceText(anchor)).join(", ")}${stale})\n ${fenceText(page.body, 600)}`;
|
|
1292
|
+
return `- ${fenceText(page.title, 240)} (${page.status} ${page.kind}, written by ${fenceText(by)}'s agent, last confirmed ${page.date || "unknown"}, about ${on.map((anchor) => fenceText(anchor)).join(", ")}${stale})\n ${fenceText(page.body, 600)}`;
|
|
1290
1293
|
});
|
|
1291
1294
|
teamLog(`${session.slice(0, 8)} ${team.project} edit ${fresh.length} delivered`);
|
|
1292
1295
|
return [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peer-thread-mark",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.32",
|
|
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"
|
|
@@ -529,7 +529,7 @@ const fenced = (text: unknown) => (typeof text === 'string' && SENTENCE.test(tex
|
|
|
529
529
|
const KNOWLEDGE = /^<teammates-project-knowledge-at-edit>\n[^<>]{1,6000}\n<\/teammates-project-knowledge-at-edit>$/
|
|
530
530
|
const fencedKnowledge = (text: unknown) => (typeof text === 'string' && KNOWLEDGE.test(text) ? text : undefined)
|
|
531
531
|
// The titles in a knowledge note, for the line the person sees under the edit.
|
|
532
|
-
const noteTitles = (text: string) => [...text.matchAll(/^- (.+?) \((?:verified|observed) /gm)].map(m => plain(m[1]!)).slice(0, 3)
|
|
532
|
+
const noteTitles = (text: string) => [...text.matchAll(/^- (.+?) \((?:verified|observed) /gm)].map(m => plain(m[1]!)).slice(0, 3) // plain() cuts each to the row's width
|
|
533
533
|
|
|
534
534
|
// The one line under an edit: another person with the warning badge, or the person's own other thread, dimmed.
|
|
535
535
|
function collisionLine(ui: { Box: (p: never) => RenderNode; Text: (p: never) => RenderNode }, surface: RenderSurface, signal: CollisionSignal, now: number, more = 0) {
|