@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.
|
|
18
|
-
so
|
|
19
|
-
|
|
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.
|
|
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
|
+
})
|