@webinson/peer 0.4.23 → 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.
|
|
@@ -27,6 +28,7 @@ In a Claude Code thread:
|
|
|
27
28
|
thread goes, on top of Claude Code's own memory. It saves only the turns from the moment it was chosen.
|
|
28
29
|
- **Inactive** (not shared): Claude Code's own memory alone; PeerMemory stays off.
|
|
29
30
|
- **Use Jellyfish** (shared): no memory at all, neither PeerMemory nor Claude Code's own (`MEMORY.md`).
|
|
31
|
+
The agent cannot read, search or change Claude Code's memory files with a tool either.
|
|
30
32
|
`CLAUDE.md` instructions stay. Claude Code puts `MEMORY.md` into a thread's first message, so a thread meant to
|
|
31
33
|
hold none starts with `/peer` (or `/peer jellyfish`): a first message with work in it already carries it.
|
|
32
34
|
|
|
@@ -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"
|
|
@@ -276,6 +276,9 @@ async function applyMemory($: EngineInterface) {
|
|
|
276
276
|
// What OpenViking's recall and Peer's team memory add to a prompt, left out where the thread keeps OpenViking off.
|
|
277
277
|
const OPENVIKING_BLOCK = /<openviking-context|<teammates-project-knowledge/
|
|
278
278
|
const OPENVIKING_TOOL = 'mcp__plugin_openviking-memory_'
|
|
279
|
+
// Claude Code's own memory files, in a tool's input however it is written: a path, a glob, a shell command that
|
|
280
|
+
// changes into ~/.claude first or looks for MEMORY.md there. A call that names both is held back.
|
|
281
|
+
const usesMemoryFiles = (input: string) => /\.claude(?=[\\/"'\s]|$)/.test(input) && /memory/i.test(input)
|
|
279
282
|
|
|
280
283
|
// The memory badge's color: an explicit value, as a surface may not know a theme key; it reads on light and dark.
|
|
281
284
|
const SAVED_COLOR = '#8b5cf6'
|
|
@@ -711,6 +714,11 @@ export const register: Register = on => {
|
|
|
711
714
|
if (e.tool.startsWith(OPENVIKING_TOOL) && (await memoryUse($)) !== 'openviking') {
|
|
712
715
|
return { deny: 'PeerMemory is off in this thread. Use Elephant turns it on.' }
|
|
713
716
|
}
|
|
717
|
+
// Jellyfish keeps Claude Code's memory files out of the context, and an agent that knows where they live
|
|
718
|
+
// (~/.claude/projects/<project>/memory) does not read, search or write them with a tool either.
|
|
719
|
+
if (usesMemoryFiles(JSON.stringify(e)) && (await memoryUse($)) === 'none') {
|
|
720
|
+
return { deny: "Jellyfish: this thread uses no memory, Claude Code's memory files included. Use Elephant or Inactive brings them back." }
|
|
721
|
+
}
|
|
714
722
|
return next(e)
|
|
715
723
|
})
|
|
716
724
|
|
|
@@ -955,6 +963,30 @@ export const register: Register = on => {
|
|
|
955
963
|
|
|
956
964
|
// /peer-list opens the pane and runs no command: the prompt never reaches the CLI's hook, so Claude Code shows
|
|
957
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
|
+
|
|
958
990
|
on('command.run', { command: 'peer-list' }, async ($, e, next) => {
|
|
959
991
|
await load($)
|
|
960
992
|
const opened = await $.ui.open({ id: PANE, title: 'Peer' })
|
|
@@ -77,12 +77,24 @@ test('Elephant turns OpenViking on, Inactive keeps Claude Code memory alone, Jel
|
|
|
77
77
|
await search()
|
|
78
78
|
expect(toolRuns).toBe(1)
|
|
79
79
|
expect((await $.prompt.section({ name: 'memory', text: 'how memory works' })).text).toBeNull()
|
|
80
|
+
// Nor its memory files, read, searched or changed with a tool; other files under ~/.claude still are.
|
|
81
|
+
const denied = async (input: object) => {
|
|
82
|
+
const before = toolRuns
|
|
83
|
+
const result = (await $.tool.call(input as never)) as { deny?: string }
|
|
84
|
+
return toolRuns === before && Boolean(result?.deny)
|
|
85
|
+
}
|
|
86
|
+
expect(await denied({ tool: 'Read', file_path: '/home/.claude/projects/p/memory/peer-proof.md' })).toBe(true)
|
|
87
|
+
expect(await denied({ tool: 'Bash', command: 'cd ~/.claude && cat projects/p/memory/MEMORY.md' })).toBe(true)
|
|
88
|
+
expect(await denied({ tool: 'Grep', pattern: '12/13', path: '/home/.claude/projects' , glob: '**/memory/*.md' })).toBe(true)
|
|
89
|
+
expect(await denied({ tool: 'Read', file_path: '/home/.claude/settings.json' })).toBe(false)
|
|
90
|
+
expect(await denied({ tool: 'Read', file_path: '/repo/.claude-plugin/memory.json' })).toBe(false)
|
|
80
91
|
const kept = (await context()).blocks.find(b => b.name === 'claudeMd')?.text ?? ''
|
|
81
92
|
expect(kept).toContain('Be brief.')
|
|
82
93
|
expect(kept).not.toContain('a fact')
|
|
83
94
|
|
|
84
95
|
// Pressing the lit Jellyfish stops sharing: Inactive, OpenViking still off, Claude Code's own memory back.
|
|
85
96
|
await band.press({ key: 'jellyfish' } as never)
|
|
97
|
+
expect(await denied({ tool: 'Read', file_path: '/home/.claude/projects/p/memory/peer-proof.md' })).toBe(false)
|
|
86
98
|
expect(env.OPENVIKING_MEMORY_ENABLED).toBe('0')
|
|
87
99
|
expect(await recalled()).toEqual(['kept'])
|
|
88
100
|
expect((await $.prompt.section({ name: 'memory', text: 'how memory works' })).text).toBe('how memory works')
|
|
@@ -168,3 +180,56 @@ test('a bare /peer turn carries no memory; the mode picked after it decides the
|
|
|
168
180
|
await $.prompt.submit({ text: '/peer fix it', command: { name: 'peer', args: 'fix it' } } as never)
|
|
169
181
|
expect(await memory()).toEqual({ section: null, fact: false })
|
|
170
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
|
+
})
|