@webinson/peer 0.4.30 → 0.4.31

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. Peer writes the note's provenance
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
@@ -1197,7 +1197,7 @@ function teamPageFields(text) {
1197
1197
  const [, kind, status, author] = meta;
1198
1198
  const contributors = (text.match(/^contributors: (.+)$/m)?.[1] ?? author).split(/,\s*/).filter(Boolean);
1199
1199
  const paths = (text.match(/^paths: (.+)$/m)?.[1] ?? "").split(/,\s*/).map((p) => p.trim()).filter((p) => p && !p.includes("..") && !p.startsWith("/"));
1200
- const title = plain(text.match(/^# (.+)$/m)?.[1] ?? "");
1200
+ const title = String(text.match(/^# (.+)$/m)?.[1] ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/g, "").slice(0, 240);
1201
1201
  const date = text.match(/last_confirmed: (\d{4}-\d{2}-\d{2})/)?.[1] ?? "";
1202
1202
  const body = text.split("\n").find((line, i) => i > 2 && line.trim() && !/^(kind|paths|contributors):/.test(line) && !line.startsWith("#")) ?? "";
1203
1203
  return { kind, status, author, contributors, paths, title, date, body: body.slice(0, 600) };
@@ -1244,8 +1244,10 @@ function refreshTeamIndex(team) {
1244
1244
  } catch {}
1245
1245
  }
1246
1246
 
1247
- // A page governs a file when one of its paths is the file or a folder above it.
1248
- const governs = (anchor, file) => anchor === file || file.startsWith(anchor.endsWith("/") ? anchor : `${anchor}/`);
1247
+ // A page governs a file when one of its paths is the file or a folder above it. A top-level folder (`cli/`) names
1248
+ // too much of the repository to say anything about one edit: such a page still comes with the prompt, not here.
1249
+ const broad = (anchor) => !anchor.replace(/\/+$/, "").includes("/") && !/\.[\w]+$/.test(anchor);
1250
+ const governs = (anchor, file) => !broad(anchor) && (anchor === file || file.startsWith(anchor.endsWith("/") ? anchor : `${anchor}/`));
1249
1251
  const fenceText = (text, max = 160) => String(text ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/[<>]/g, "").trim().slice(0, max);
1250
1252
 
1251
1253
  // The fenced note for the agent after an edit of `files` (absolute or relative to `cwd`), or undefined.
@@ -1286,7 +1288,7 @@ function teamNotesAt({ session, cwd, files }) {
1286
1288
  const on = page.paths.filter((anchor) => relative.some((file) => governs(anchor, file)));
1287
1289
  const changed = changesSince(base, page.paths, page.date);
1288
1290
  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)}`;
1291
+ 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
1292
  });
1291
1293
  teamLog(`${session.slice(0, 8)} ${team.project} edit ${fresh.length} delivered`);
1292
1294
  return [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peer-thread-mark",
3
- "version": "0.4.30",
3
+ "version": "0.4.31",
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webinson/peer",
3
- "version": "0.4.30",
3
+ "version": "0.4.31",
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",