@webinson/peer 0.4.24 → 0.4.25

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
@@ -14,9 +14,10 @@ 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. 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.
17
+ - `/peer` shares this thread in your workspace. With the band installed it is answered without a turn of the
18
+ model, so nothing reaches the agent yet: Claude Code builds a thread's first context before any command runs,
19
+ and a thread you then switch to Jellyfish holds no memory from the start.
20
+ - `/peer jellyfish` and `/peer elephant` share it in that memory mode at once, also without a turn.
20
21
  - `/peer <message>` shares it and passes the message on to the agent as your prompt.
21
22
  - `/peer stop` ends sharing. Quitting or restarting Claude Code only pauses it: the thread shows as stopped and
22
23
  is shared again when the session is resumed. `/clear` and signing out of Claude Code end it.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peer-thread-mark",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
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"
@@ -963,6 +963,30 @@ export const register: Register = on => {
963
963
 
964
964
  // /peer-list opens the pane and runs no command: the prompt never reaches the CLI's hook, so Claude Code shows
965
965
  // no "hook blocked your prompt" card. Where no surface can seat a pane, the command runs as before.
966
+ // `/peer`, `/peer jellyfish`, `/peer elephant` share the thread here, with no turn of the model: Claude Code
967
+ // builds a conversation's first context before any command runs, so a turn would carry MEMORY.md before the mode
968
+ // is known. Without a turn nothing is sent, and the first real message is built in the mode picked here.
969
+ on('command.run', { command: 'peer' }, async ($, e, next) => {
970
+ const word = e.args.trim().toLowerCase()
971
+ if (word !== '' && word !== 'jellyfish' && word !== 'elephant') return next(e)
972
+ try {
973
+ const argv = await promptHook($)
974
+ if (!argv) return next(e)
975
+ const input = { session_id: await $.session.id(), cwd: await $.env.get('PWD'), prompt: '/peer' }
976
+ const { stdout } = await $.process.run(argv, { stdin: JSON.stringify(input), timeoutMs: 10_000 })
977
+ const answer = JSON.parse(stdout.trim().split('\n').pop() || '{}') as { reason?: string; hookSpecificOutput?: { additionalContext?: string } }
978
+ const line = (answer.hookSpecificOutput?.additionalContext ?? '').split('\n').find(l => l.startsWith('Peer: this thread is shared'))
979
+ if (!line) return { text: answer.reason ?? 'Peer: the thread was not shared; `peer doctor` says why.' }
980
+ if (word) await update($, mode, () => word)
981
+ await refresh($)
982
+ $.ui.invalidate('prompt.section')
983
+ $.ui.invalidate('prompt.context')
984
+ return { text: `${line}\n${await memoryNote($)}` }
985
+ } catch {
986
+ return next(e)
987
+ }
988
+ })
989
+
966
990
  on('command.run', { command: 'peer-list' }, async ($, e, next) => {
967
991
  await load($)
968
992
  const opened = await $.ui.open({ id: PANE, title: 'Peer' })
@@ -180,3 +180,56 @@ test('a bare /peer turn carries no memory; the mode picked after it decides the
180
180
  await $.prompt.submit({ text: '/peer fix it', command: { name: 'peer', args: 'fix it' } } as never)
181
181
  expect(await memory()).toEqual({ section: null, fact: false })
182
182
  })
183
+
184
+ test('/peer jellyfish shares the thread with no turn of the model, so its first message is built without memory', async ($, on) => {
185
+ let shared = false
186
+ const prompts: string[] = []
187
+ on('env.get', () => ({ value: '/home' }) as never)
188
+ on('fs.read', (_, e) => {
189
+ const path = String((e as { path?: string }).path ?? '')
190
+ if (path.endsWith('settings.json')) return { value: JSON.stringify({ hooks: { UserPromptSubmit: [{ hooks: [{ command: '"/n/node" "/p/peer.mjs" hook prompt # peer-hook' }] }] } }) } as never
191
+ if (path.endsWith('/sessions/s1') && shared) return { value: JSON.stringify({ workspace: 'hotovo' }) } as never
192
+ throw new Error('none')
193
+ })
194
+ on('process.run', (_, e) => {
195
+ const stdin = (e as { init?: { stdin?: string } }).init?.stdin
196
+ const prompt = stdin ? (JSON.parse(stdin) as { prompt?: string }).prompt : undefined
197
+ if (prompt) {
198
+ prompts.push(prompt)
199
+ shared = true
200
+ const additionalContext = "[Peer's hook ran]\nPeer: this thread is shared in hotovo as peer @ main. /peer stop ends it.\nThe person typed /peer alone."
201
+ return { value: { stdout: JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext } }), stderr: '', exitCode: 0 } } as never
202
+ }
203
+ return { value: { stdout: JSON.stringify({ connected: true, saves: [] }), stderr: '', exitCode: 0 } } as never
204
+ })
205
+ on('env.set', () => ({ value: {} }) as never)
206
+ on('session.id', () => ({ value: 's1' }) as never)
207
+ on('session.cwd', () => ({ value: '/repo' }) as never)
208
+ on('clock.now', () => ({ value: 1 }) as never)
209
+ on('command.register', (_, e) => ({ value: { command: (e as { name: string }).name } }) as never)
210
+ on('ui.status', () => ({ value: undefined }) as never)
211
+ on('ui.invalidate', () => ({ value: undefined }) as never)
212
+ on('ui.toast', () => ({ value: undefined }) as never)
213
+ on('session.start', (_, e) => ({ cwd: e.cwd }))
214
+ let modelTurns = 0
215
+ on('command.run', () => { modelTurns++; return { text: '' } as never })
216
+ on('prompt.section', () => ({ text: 'how memory works' }) as never)
217
+ on('prompt.context', (_, e) => ({ blocks: (e as { blocks: unknown[] }).blocks }) as never)
218
+
219
+ await $.session.start({ cwd: '/', surface: 'desktop', isInteractive: true })
220
+ const said = await $.command.run({ command: 'peer', args: 'jellyfish' } as never)
221
+ expect(modelTurns).toBe(0)
222
+ expect(prompts).toEqual(['/peer'])
223
+ expect(String((said as { text?: string }).text)).toContain('Peer: this thread is shared in hotovo')
224
+ expect(String((said as { text?: string }).text)).toContain('Jellyfish')
225
+ expect((await $.prompt.section({ name: 'memory', text: 'how memory works' })).text).toBeNull()
226
+ const context = await $.prompt.context({
227
+ blocks: [{ name: 'claudeMd', text: CLAUDE_MD }],
228
+ instructionFiles: [{ path: MEMORY_FILE, kind: 'memory', content: '- a fact' }],
229
+ })
230
+ expect(context.blocks.find(b => b.name === 'claudeMd')?.text).not.toContain('a fact')
231
+
232
+ // A message after /peer is the person's work: the engine's command runs as before.
233
+ await $.command.run({ command: 'peer', args: 'fix the export' } as never)
234
+ expect(modelTurns).toBe(1)
235
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webinson/peer",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
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",