@webinson/peer 0.4.1 → 0.4.2

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
@@ -18,6 +18,9 @@ In a Claude Code thread:
18
18
  - `/peer <message>` shares it and passes the message on to the agent as your prompt.
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
+ - In a shared thread a band above the prompt says `Peer <workspace>` with `/peer stop` to click; after a stop it
22
+ turns gray with `/peer start`. It is a Claude Code plugin in this package (`mod/`) that `peer install` adds;
23
+ the desktop app draws it only while you type or a turn runs.
21
24
  - A shared thread's title starts with `● Peer · `, so it stands out in Claude Code's session list; the mark comes
22
25
  back if Claude Code renames the thread, and `/peer stop` takes it off.
23
26
  - `/peer-list` lists the shared threads by repository, then by person, with each thread's branch and
package/bin/peer.mjs CHANGED
@@ -21,6 +21,12 @@ const HOOK_TIMEOUT_S = 10;
21
21
  const HUB_TIMEOUT_MS = 3000;
22
22
  // Why a hook did what it did, without any prompt text: `peer doctor` shows the end of it.
23
23
  const HOOK_LOG = path.join(HOME, "hook.log");
24
+ // The thread mark: a Claude Code plugin shipped in this package (mod/), a marketplace of one plugin read
25
+ // from that folder, so updating the package updates it.
26
+ const CLAUDE_BIN = process.env.PEER_CLAUDE || "claude";
27
+ const MOD_DIR = path.join(path.dirname(SELF), "..", "mod");
28
+ const MOD = "peer-thread-mark";
29
+ const MARKETPLACE = "webinson-peer";
24
30
 
25
31
  const HELP = `peer: share chosen agent threads with your team
26
32
 
@@ -33,7 +39,7 @@ const HELP = `peer: share chosen agent threads with your team
33
39
  connect to the team's memory server (your key on stdin); new threads in the
34
40
  project's repositories then start with teammates' published findings
35
41
  peer team status the team server, project and the last recalls
36
- peer install set up Claude Code again (hooks and the two commands)
42
+ peer install set up Claude Code again (hooks, the two commands and the thread mark)
37
43
  peer uninstall remove them from Claude Code
38
44
  peer logout forget the sign-in on this computer
39
45
 
@@ -372,13 +378,49 @@ function install() {
372
378
  fs.mkdirSync(COMMANDS, { recursive: true });
373
379
  for (const [name, text] of Object.entries(COMMAND_FILES)) fs.writeFileSync(path.join(COMMANDS, name), text);
374
380
  console.log(`Claude Code: hooks in ${SETTINGS}, /peer and /peer-list in ${COMMANDS}. Sessions started from now on have them.`);
381
+ installMark();
382
+ }
383
+
384
+ function claudeCli(args) {
385
+ return execFileSync(CLAUDE_BIN, args, { encoding: "utf8", timeout: 60000, stdio: ["ignore", "pipe", "pipe"] });
386
+ }
387
+
388
+ // The band above the prompt in shared threads. Optional: /peer works without it, so a failure only says so.
389
+ function installMark() {
390
+ if (!fs.existsSync(path.join(MOD_DIR, ".claude-plugin", "marketplace.json"))) return;
391
+ const install = () => claudeCli(["plugin", "install", MOD, "--marketplace", MOD_DIR, "--scope", "user"]);
392
+ try {
393
+ try {
394
+ install();
395
+ } catch {
396
+ // A marketplace of that name added from an older place (another install of this package) is replaced.
397
+ try {
398
+ claudeCli(["plugin", "marketplace", "remove", MARKETPLACE]);
399
+ } catch {}
400
+ install();
401
+ }
402
+ console.log(`Claude Code: the thread mark (${MOD}), a band above the prompt in shared threads, from ${MOD_DIR}.`);
403
+ } catch (error) {
404
+ const reason = String(error.stderr || error.message).trim().split("\n").pop();
405
+ console.log(`Claude Code: the thread mark was not installed (${reason}); /peer works without it.`);
406
+ console.log(` To add it later: claude plugin install ${MOD} --marketplace "${MOD_DIR}"`);
407
+ }
408
+ }
409
+
410
+ function uninstallMark() {
411
+ for (const args of [["plugin", "uninstall", `${MOD}@${MARKETPLACE}`], ["plugin", "marketplace", "remove", MARKETPLACE]]) {
412
+ try {
413
+ claudeCli(args);
414
+ } catch {}
415
+ }
375
416
  }
376
417
 
377
418
  function uninstall() {
378
419
  const settings = readJson(SETTINGS);
379
420
  if (settings) writeJson(SETTINGS, withoutOurHooks(settings), 0o644);
380
421
  for (const name of Object.keys(COMMAND_FILES)) fs.rmSync(path.join(COMMANDS, name), { force: true });
381
- console.log("Claude Code: Peer's hooks and commands removed.");
422
+ uninstallMark();
423
+ console.log("Claude Code: Peer's hooks, commands and thread mark removed.");
382
424
  }
383
425
 
384
426
  // ---- signing in ----
@@ -470,8 +512,11 @@ async function doctor() {
470
512
  }
471
513
 
472
514
  try {
473
- const version = execFileSync("claude", ["--version"], { encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "ignore"] }).trim();
515
+ const version = execFileSync(CLAUDE_BIN, ["--version"], { encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "ignore"] }).trim();
474
516
  ok(`Claude Code ${version} (Peer is tested with 2.1.316)`);
517
+ const mark = JSON.parse(claudeCli(["plugin", "list", "--json"])).find((p) => p.id === `${MOD}@${MARKETPLACE}`);
518
+ if (mark?.enabled) ok(`the thread mark (${MOD}) is installed`);
519
+ else console.log(` -- the thread mark (${MOD}, a band above the prompt) is ${mark ? "turned off" : "not installed"}: \`peer install\` adds it`);
475
520
  } catch {
476
521
  console.log(" ? `claude` is not on this terminal's PATH, so its version is unknown");
477
522
  }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "webinson-peer",
3
+ "owner": { "name": "Webinson" },
4
+ "plugins": [
5
+ {
6
+ "name": "peer-thread-mark",
7
+ "source": "./peer-thread-mark",
8
+ "description": "Marks a Claude Code thread shared with Peer (/peer) with a band above the prompt"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "peer-thread-mark",
3
+ "version": "0.4.2",
4
+ "description": "Marks a Claude Code thread shared with Peer (/peer) with a band above the prompt, with /peer stop and /peer start",
5
+ "license": "MIT",
6
+ "types": "./types/index.d.ts"
7
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.tsx"] }
@@ -0,0 +1,115 @@
1
+ import { atom, read, update } from 'claude-code'
2
+ import type { EngineInterface, Register } from 'claude-code'
3
+
4
+ import type { Shared } from '../types'
5
+
6
+ // The peer CLI's hook writes ~/.peer/sessions/<session id> on `/peer` and removes it on `/peer stop`;
7
+ // that file alone says whether this thread is shared, and in which workspace.
8
+ const shared = atom({ plugin: 'peer-thread-mark', key: 'shared' } as const, null)
9
+
10
+ async function peerHome($: EngineInterface) {
11
+ return (await $.env.get('PEER_HOME')) || `${await $.env.get('HOME')}/.peer`
12
+ }
13
+
14
+ async function refresh($: EngineInterface) {
15
+ const home = await peerHome($)
16
+ const before = await read($, shared)
17
+ let now: Shared
18
+ try {
19
+ const marker = JSON.parse(await $.fs.read(`${home}/sessions/${await $.session.id()}`))
20
+ now = { workspace: String(marker.workspace ?? 'Peer'), isShared: true }
21
+ } catch {
22
+ // Not shared, or not readable: keep the band and the workspace it last showed, so stop and start differ
23
+ // only in state. A thread never shared on a computer without Peer stays without a band.
24
+ now = before ? { workspace: before.workspace, isShared: false } : null
25
+ }
26
+ if (before?.workspace !== now?.workspace || before?.isShared !== now?.isShared) {
27
+ await update($, shared, () => now)
28
+ }
29
+ }
30
+
31
+ // The prompt hook exactly as `peer install` put it in Claude Code's settings, split into its arguments.
32
+ async function promptHook($: EngineInterface): Promise<string[] | undefined> {
33
+ const claude = (await $.env.get('CLAUDE_CONFIG_DIR')) || `${await $.env.get('HOME')}/.claude`
34
+ const settings = JSON.parse(await $.fs.read(`${claude}/settings.json`))
35
+ for (const group of settings.hooks?.UserPromptSubmit ?? []) {
36
+ for (const hook of group.hooks ?? []) {
37
+ const command = String(hook.command ?? '')
38
+ if (!command.includes('# peer-hook') || !/\bhook prompt\b/.test(command)) continue
39
+ return [...command.split('#')[0].matchAll(/"([^"]*)"|(\S+)/g)].map(m => m[1] ?? m[2])
40
+ }
41
+ }
42
+ return undefined
43
+ }
44
+
45
+ // A press does what typing the command does: the CLI's own hook shares or stops the thread and says so.
46
+ // (A plugin may not submit a prompt that starts with `/`, so the hook is run directly.)
47
+ async function send($: EngineInterface, prompt: string) {
48
+ try {
49
+ const argv = await promptHook($)
50
+ if (!argv) {
51
+ $.ui.toast('Peer: no prompt hook in Claude Code settings; run `peer install`')
52
+ return
53
+ }
54
+ const input = { session_id: await $.session.id(), cwd: await $.env.get('PWD'), prompt }
55
+ const { stdout } = await $.process.run(argv, { stdin: JSON.stringify(input), timeoutMs: 10_000 })
56
+ const reason = JSON.parse(stdout.trim().split('\n').pop() || '{}').reason
57
+ if (reason) $.ui.toast(String(reason))
58
+ } catch (error) {
59
+ $.ui.toast(`Peer: ${error instanceof Error ? error.message : String(error)}`)
60
+ }
61
+ await refresh($)
62
+ }
63
+
64
+ export const register: Register = on => {
65
+ // A session that loaded the mod after it started (/reload-plugins, an install) has no state yet: the first
66
+ // drawing looks once, so the band shows without waiting for the next prompt.
67
+ let looked = false
68
+ on('session.start', async ($, e, next) => {
69
+ const result = await next(e)
70
+ // Nothing under the prompt: clear the line an earlier version pinned there.
71
+ $.ui.status(undefined)
72
+ await refresh($).catch(() => {})
73
+ return result
74
+ })
75
+
76
+ // The CLI's prompt hook writes or removes the marker while a typed command goes in, so look after it.
77
+ on('prompt.submit', async ($, e, next) => {
78
+ const result = await next(e)
79
+ await refresh($).catch(() => {})
80
+ return result
81
+ })
82
+
83
+ on('turn.complete', async ($, e, next) => {
84
+ await refresh($).catch(() => {})
85
+ return next(e)
86
+ })
87
+
88
+ on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
89
+ if (!looked) {
90
+ looked = true
91
+ await refresh($).catch(() => {})
92
+ }
93
+ const now = await read($, shared)
94
+ if (e.props.hasSurvey || now === null) return next(e)
95
+
96
+ const { Box, Button, Text } = $.ui.resolve(e)
97
+ // One band in both states: the label's own surface says the state (accent when shared, gray when not),
98
+ // and the one control changes in place, stop to start and back.
99
+ return (
100
+ <Box>
101
+ <Text backgroundColor={now.isShared ? 'suggestion' : 'inactive'} color="inverseText" bold>
102
+ {' Peer '}
103
+ </Text>
104
+ <Text bold={now.isShared} dimColor={!now.isShared}>{now.workspace ? ` ${now.workspace} ` : ' '}</Text>
105
+ <Button
106
+ key="toggle"
107
+ label={now.isShared ? '/peer stop' : '/peer start'}
108
+ plain
109
+ dimColor
110
+ onPress={() => send($, now.isShared ? '/peer stop' : '/peer')}
111
+ />
112
+ </Box>
113
+ )
114
+ })
115
+ }
@@ -0,0 +1,9 @@
1
+ // Whether this thread is shared, in which Peer workspace; null when Peer is not signed in on this computer
2
+ // (the band then draws nothing).
3
+ export type Shared = { workspace: string; isShared: boolean } | null
4
+
5
+ declare module 'claude-code' {
6
+ interface PluginState {
7
+ 'peer-thread-mark': { shared: Shared }
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webinson/peer",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Share chosen Claude Code threads with your team: who has agents running and on what.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "bin",
12
+ "mod",
12
13
  "README.md"
13
14
  ],
14
15
  "engines": {