@webinson/peer 0.4.2 → 0.4.14
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 +23 -7
- package/bin/peer.mjs +512 -13
- package/mod/peer-thread-mark/.claude-plugin/plugin.json +2 -2
- package/mod/peer-thread-mark/hooks/register.tsx +884 -12
- package/mod/peer-thread-mark/tests/all.test.tsx +107 -0
- package/mod/peer-thread-mark/tests/band.test.tsx +59 -0
- package/mod/peer-thread-mark/tests/memories.test.tsx +54 -0
- package/mod/peer-thread-mark/tests/recall.test.tsx +70 -0
- package/mod/peer-thread-mark/tests/saved.test.tsx +148 -0
- package/mod/peer-thread-mark/types/index.d.ts +62 -1
- package/package.json +1 -1
package/bin/peer.mjs
CHANGED
|
@@ -34,11 +34,15 @@ const HELP = `peer: share chosen agent threads with your team
|
|
|
34
34
|
sign in once and set up Claude Code (/peer, /peer stop, /peer-list)
|
|
35
35
|
peer list people in the workspace, their shared threads and what they work on
|
|
36
36
|
peer status who you are, the workspace and the threads this computer shares
|
|
37
|
-
peer doctor why
|
|
37
|
+
peer doctor why Peer's hook did not answer /peer
|
|
38
38
|
peer team connect <url> --as <member> [--project <id>]
|
|
39
39
|
connect to the team's memory server (your key on stdin); new threads in the
|
|
40
|
-
project's repositories then start with teammates' published findings
|
|
40
|
+
project's repositories then start with teammates' published findings. The
|
|
41
|
+
project is the Hub project holding this repository, else the repository itself
|
|
41
42
|
peer team status the team server, project and the last recalls
|
|
43
|
+
peer all [words] [here] [--newest | --oldest] [page N]
|
|
44
|
+
your own memories in your personal OpenViking, or the ones with these words
|
|
45
|
+
(full text); here: only this repository's. /peer-all in Claude Code
|
|
42
46
|
peer install set up Claude Code again (hooks, the two commands and the thread mark)
|
|
43
47
|
peer uninstall remove them from Claude Code
|
|
44
48
|
peer logout forget the sign-in on this computer
|
|
@@ -242,6 +246,11 @@ async function hook(event) {
|
|
|
242
246
|
const prompt = String(input.prompt ?? "").trim();
|
|
243
247
|
const command = /^\/peer(?:\s+([\s\S]*))?$/.exec(prompt);
|
|
244
248
|
const argument = command?.[1]?.trim() ?? "";
|
|
249
|
+
const all = /^\/peer-all(?:\s+([\s\S]*))?$/.exec(prompt);
|
|
250
|
+
if (all) {
|
|
251
|
+
const query = memoryQuery((all[1] ?? "").split(/\s+/).filter(Boolean), input.cwd || process.cwd());
|
|
252
|
+
return block(await memoryText(query).catch((error) => `Peer: your memories did not load (${error.message})`));
|
|
253
|
+
}
|
|
245
254
|
if (prompt === "/peer-list") {
|
|
246
255
|
let saved;
|
|
247
256
|
try {
|
|
@@ -273,9 +282,11 @@ async function hook(event) {
|
|
|
273
282
|
await report(saved, session, argument ? "working" : "idle", input.cwd).catch((error) => (hubError = error));
|
|
274
283
|
const said = hubError ? `Peer: shared, but the hub did not answer yet (${hubError.message}). The next turn tries again.` : sharedText;
|
|
275
284
|
const sessionTitle = retitle(input, true, what);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
285
|
+
// A bare /peer reaches the agent too, which confirms in one line: a block would show the person Claude
|
|
286
|
+
// Code's "hook blocked your prompt" warning for what went right.
|
|
287
|
+
const note = argument ? said : `${said}\n${ALONE_NOTE}`;
|
|
288
|
+
log(argument ? "answered /peer with a message: shared, message passed on" : "answered /peer: shared, the agent confirms");
|
|
289
|
+
return answer({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: `${SHARED_NOTE}\n${note}`, ...(sessionTitle ? { sessionTitle } : {}) } });
|
|
279
290
|
}
|
|
280
291
|
if (shared) await reportQuietly(session, "working", input.cwd);
|
|
281
292
|
// Claude Code may name a thread after its first prompts; the mark goes back on, or comes off a thread
|
|
@@ -322,6 +333,10 @@ const COMMANDS = path.join(CLAUDE, "commands");
|
|
|
322
333
|
|
|
323
334
|
// The hook's note to the model when `/peer <message>` shares a thread; peer.md looks for it.
|
|
324
335
|
const SHARED_NOTE = "[Peer's hook ran]";
|
|
336
|
+
// Added when /peer came alone: there is no message to work on, only the confirmation, and its privacy line
|
|
337
|
+
// is a promise the person relies on, so it stays whole.
|
|
338
|
+
const ALONE_NOTE =
|
|
339
|
+
"The person typed /peer alone. Reply with only that confirmation, in one or two short lines in the person's language: where the thread is shared, that teammates see its repository, branch and state but never the conversation, and that /peer stop ends it. Do nothing else and use no tools.";
|
|
325
340
|
|
|
326
341
|
const COMMAND_FILES = {
|
|
327
342
|
"peer.md": `---
|
|
@@ -340,6 +355,14 @@ description: List the people in your Peer workspace and the agent threads they s
|
|
|
340
355
|
Peer's hook answers this command before it reaches you, so you should not be reading it. If you are,
|
|
341
356
|
the hook did not run or did not answer in time: tell the person to run \`peer doctor\` in a terminal, which says why.
|
|
342
357
|
Do nothing else.
|
|
358
|
+
`,
|
|
359
|
+
"peer-all.md": `---
|
|
360
|
+
description: List and search what your own OpenViking memory holds (/peer-all [words] [here] [page N])
|
|
361
|
+
argument-hint: "[words] [here] [page N]"
|
|
362
|
+
---
|
|
363
|
+
Peer's hook answers this command before it reaches you, so you should not be reading it. If you are,
|
|
364
|
+
the hook did not run or did not answer in time: tell the person to run \`peer doctor\` in a terminal, which says why.
|
|
365
|
+
Do nothing else.
|
|
343
366
|
`,
|
|
344
367
|
};
|
|
345
368
|
|
|
@@ -377,7 +400,7 @@ function install() {
|
|
|
377
400
|
writeJson(SETTINGS, settings, 0o644);
|
|
378
401
|
fs.mkdirSync(COMMANDS, { recursive: true });
|
|
379
402
|
for (const [name, text] of Object.entries(COMMAND_FILES)) fs.writeFileSync(path.join(COMMANDS, name), text);
|
|
380
|
-
console.log(`Claude Code: hooks in ${SETTINGS}, /peer and /peer-
|
|
403
|
+
console.log(`Claude Code: hooks in ${SETTINGS}, /peer, /peer-list and /peer-all in ${COMMANDS}. Sessions started from now on have them.`);
|
|
381
404
|
installMark();
|
|
382
405
|
}
|
|
383
406
|
|
|
@@ -488,7 +511,7 @@ function status() {
|
|
|
488
511
|
console.log(shared.length ? `Threads shared from this computer: ${shared.join(", ")}` : "No thread shared from this computer.");
|
|
489
512
|
}
|
|
490
513
|
|
|
491
|
-
// ---- peer doctor: why
|
|
514
|
+
// ---- peer doctor: why Peer's hook did not answer /peer ----
|
|
492
515
|
|
|
493
516
|
async function doctor() {
|
|
494
517
|
let problems = 0;
|
|
@@ -545,7 +568,7 @@ async function doctor() {
|
|
|
545
568
|
}
|
|
546
569
|
if (process.env.CLAUDE_CONFIG_DIR) console.log(` note CLAUDE_CONFIG_DIR=${process.env.CLAUDE_CONFIG_DIR}: Claude Code must run with the same value`);
|
|
547
570
|
const commands = Object.keys(COMMAND_FILES).filter((name) => fs.existsSync(path.join(COMMANDS, name)));
|
|
548
|
-
if (commands.length === Object.keys(COMMAND_FILES).length) ok(`/peer and /peer-
|
|
571
|
+
if (commands.length === Object.keys(COMMAND_FILES).length) ok(`/peer, /peer-list and /peer-all in ${COMMANDS}`);
|
|
549
572
|
else bad(`commands missing in ${COMMANDS}: run \`peer install\``);
|
|
550
573
|
|
|
551
574
|
// The hook Claude Code would run, run here: a /peer-list must come back answered within Claude's limit.
|
|
@@ -571,11 +594,287 @@ async function doctor() {
|
|
|
571
594
|
console.log(
|
|
572
595
|
problems
|
|
573
596
|
? `\n${problems} thing(s) to fix. Then start a new Claude Code session.`
|
|
574
|
-
: "\nPeer is set up here. If
|
|
597
|
+
: "\nPeer is set up here. If the agent still says the hook did not run, Claude Code is not loading your user settings: an app or IDE that runs Claude through the Agent SDK may ignore ~/.claude/settings.json. Start `claude` in a terminal and try /peer there.",
|
|
575
598
|
);
|
|
576
599
|
if (problems) process.exitCode = 1;
|
|
577
600
|
}
|
|
578
601
|
|
|
602
|
+
// ---- this thread's memories: what the person's own OpenViking kept from a Claude Code thread ----
|
|
603
|
+
// The OpenViking plugin stores each Claude Code thread as session `cc-<session id>`; every commit leaves a
|
|
604
|
+
// memory_diff.json with what it added, updated or removed. `peer thread` reads those and says them in sentences,
|
|
605
|
+
// with no model call. Read-only: it never changes a memory.
|
|
606
|
+
|
|
607
|
+
const OV_CONF = process.env.PEER_OV_CONF || path.join(os.homedir(), ".openviking", "ovcli.conf");
|
|
608
|
+
|
|
609
|
+
async function ovRead(ov, route) {
|
|
610
|
+
const response = await fetch(`${ov.url}/api/v1${route}`, {
|
|
611
|
+
headers: { "x-api-key": ov.api_key ?? "", "x-openviking-account": ov.account ?? "default", "x-openviking-user": ov.user ?? "" },
|
|
612
|
+
signal: AbortSignal.timeout(5000),
|
|
613
|
+
});
|
|
614
|
+
const value = await response.json().catch(() => ({}));
|
|
615
|
+
if (!response.ok || value.status !== "ok") throw new Error(value.error?.message ?? `OpenViking answered ${response.status}`);
|
|
616
|
+
return value.result;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// The memory's name in words: `entities/software_system/peer.md` is "peer", `.../autonomous_auditing.md` "autonomous auditing".
|
|
620
|
+
const memoryName = (uri) => path.basename(String(uri), ".md").replace(/[_-]+/g, " ");
|
|
621
|
+
|
|
622
|
+
// The sentence a memory starts with: OpenViking writes it under `# Summary`; otherwise its first line of text.
|
|
623
|
+
function summaryOf(text) {
|
|
624
|
+
const lines = String(text ?? "").split("\n").map((line) => line.trim());
|
|
625
|
+
const at = lines.indexOf("# Summary");
|
|
626
|
+
const first = (at >= 0 ? lines.slice(at + 1) : lines).find((line) => line && !line.startsWith("#"));
|
|
627
|
+
return (first ?? "").replace(/^[-*]\s+/, "");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// What an update added: the lines in `after` that `before` did not have.
|
|
631
|
+
function addedLines(before, after) {
|
|
632
|
+
const had = new Set(String(before ?? "").split("\n").map((line) => line.trim()));
|
|
633
|
+
return String(after ?? "").split("\n").map((line) => line.trim()).filter((line) => line && !had.has(line) && !line.startsWith("#")).map((line) => line.replace(/^[-*]\s+/, ""));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// A memory's own text as Markdown, for a card that shows it: without the conversation OpenViking quotes under it
|
|
637
|
+
// (`# … ChatLog:`), without its `# Summary` heading and without a first heading that only repeats its name.
|
|
638
|
+
function memoryBody(text, name) {
|
|
639
|
+
const lines = String(text ?? "").split("\n");
|
|
640
|
+
const cut = lines.findIndex((line) => /^#+ .*ChatLog/i.test(line.trim()));
|
|
641
|
+
const kept = (cut >= 0 ? lines.slice(0, cut) : lines).filter((line) => line.trim() !== "# Summary");
|
|
642
|
+
if (kept[0] && /^# /.test(kept[0]) && kept[0].slice(2).trim().toLowerCase().replace(/[_-]+/g, " ") === String(name).toLowerCase()) kept.shift();
|
|
643
|
+
return kept.join("\n").replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "").trim().slice(0, 3000);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Text Claude Code wrote into the turn, not the person: kept by the extraction, flagged so the person sees it.
|
|
647
|
+
const hasSystemText = (text) => /This is how Claude Code surfaces messages|<system-reminder>/.test(String(text ?? ""));
|
|
648
|
+
|
|
649
|
+
// The thread's own messages as Claude Code keeps them (~/.claude/projects/<project>/<session>.jsonl): the person's
|
|
650
|
+
// prompts with the id their row in the thread has, and when they were sent.
|
|
651
|
+
// What a find in the thread meets, in order: the person's own messages and the agent's written replies, with the
|
|
652
|
+
// Markdown marks a view does not draw taken out, cased alike.
|
|
653
|
+
// Curly quotes count as straight ones and accents as their letters (pamat finds pamäť), as the desktop's find treats them.
|
|
654
|
+
const visible = (text) =>
|
|
655
|
+
String(text ?? "")
|
|
656
|
+
.normalize("NFD")
|
|
657
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
658
|
+
.replace(/[\u2018\u2019\u201b\u2032]/g, "'")
|
|
659
|
+
.replace(/[\u201c\u201d\u201f\u2033]/g, '"')
|
|
660
|
+
.replace(/[*`#>_~]/g, "")
|
|
661
|
+
.replace(/\s+/g, " ")
|
|
662
|
+
.toLowerCase();
|
|
663
|
+
|
|
664
|
+
function transcriptPrompts(session) {
|
|
665
|
+
const projects = path.join(CLAUDE, "projects");
|
|
666
|
+
let file;
|
|
667
|
+
try {
|
|
668
|
+
file = fs.readdirSync(projects).map((dir) => path.join(projects, dir, `${session}.jsonl`)).find((f) => fs.existsSync(f));
|
|
669
|
+
} catch {}
|
|
670
|
+
if (!file) return [];
|
|
671
|
+
const prompts = [];
|
|
672
|
+
const shown = [];
|
|
673
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
674
|
+
let row;
|
|
675
|
+
try {
|
|
676
|
+
row = JSON.parse(line);
|
|
677
|
+
} catch {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
// The person's own messages only: not what a skill or a command put in the thread (isMeta), not a command's row.
|
|
681
|
+
// The agent's replies and the one-line descriptions of its steps are drawn too, and a find meets them.
|
|
682
|
+
if (row.type === "assistant" && Array.isArray(row.message?.content)) {
|
|
683
|
+
for (const block of row.message.content) {
|
|
684
|
+
if (block?.type === "text" && block.text) shown.push(visible(block.text));
|
|
685
|
+
else if (block?.type === "tool_use" && typeof block.input?.description === "string") shown.push(visible(block.input.description));
|
|
686
|
+
}
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
// A command's output (`system`, local_command) is drawn in the thread as well.
|
|
690
|
+
if (row.type === "system" && typeof row.content === "string") {
|
|
691
|
+
shown.push(visible(row.content.replace(/<[^>]+>/g, " ")));
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (row.type !== "user" || !row.uuid) continue;
|
|
695
|
+
const content = row.message?.content;
|
|
696
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter((b) => b?.type === "text").map((b) => b.text).join("\n") : "";
|
|
697
|
+
// A command's row and its output show in the thread, so they count for a find, but they are not messages.
|
|
698
|
+
if (row.isMeta || /^\s*<(command-name|local-command)/.test(text)) {
|
|
699
|
+
if (!row.isMeta || /^\s*<local-command-stdout/.test(text)) shown.push(visible(text.replace(/<[^>]+>/g, " ")));
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (!text) continue;
|
|
703
|
+
// Pasted text shows as an attachment, so the words a find can meet are the person's own beside it.
|
|
704
|
+
const own = text.replace(/<pasted_content[^>]*>[\s\S]*?<\/pasted_content[^>]*>/g, "").trim();
|
|
705
|
+
const keys = [...new Set([promptKey(text), promptKey(own)])].filter(Boolean);
|
|
706
|
+
if (own) {
|
|
707
|
+
prompts.push({ id: row.uuid, at: Date.parse(row.timestamp ?? "") || 0, keys, own, shownAt: shown.length });
|
|
708
|
+
shown.push(visible(own));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Each message's find: one of its own lines whole, the one the thread holds the fewest times (the longest of
|
|
712
|
+
// those), and which match, in the thread's order, is this message. A line, as a find does not match across lines.
|
|
713
|
+
const count = (needle, upTo = shown.length) => shown.slice(0, upTo).reduce((n, text) => n + text.split(needle).length - 1, 0);
|
|
714
|
+
for (const prompt of prompts) {
|
|
715
|
+
const lines = prompt.own.split("\n").map((line) => line.trim().replace(/\s+/g, " ")).filter((line) => visible(line).length >= 3);
|
|
716
|
+
let best;
|
|
717
|
+
for (const line of lines) {
|
|
718
|
+
const times = count(visible(line));
|
|
719
|
+
if (!best || times < best.times || (times === best.times && line.length > best.line.length)) best = { line, times };
|
|
720
|
+
}
|
|
721
|
+
const text = (best?.line ?? findPhrase(prompt.own)).slice(0, 500);
|
|
722
|
+
prompt.find = { text, match: count(visible(text), prompt.shownAt) + 1 };
|
|
723
|
+
}
|
|
724
|
+
return prompts;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// The words a find looks for: the message's first line, at most 60 characters, cut where a word ends.
|
|
728
|
+
function findPhrase(text) {
|
|
729
|
+
const line = String(text).trim().split("\n")[0].replace(/\s+/g, " ");
|
|
730
|
+
if (line.length <= 60) return line;
|
|
731
|
+
const cut = line.slice(0, 60);
|
|
732
|
+
return cut.slice(0, cut.lastIndexOf(" ") > 30 ? cut.lastIndexOf(" ") : 60);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// One message's text as both sides can match it: its first line, spaces collapsed, cased alike, cut short.
|
|
736
|
+
const promptKey = (text) => String(text ?? "").trim().split("\n")[0].replace(/\s+/g, " ").toLowerCase().slice(0, 80);
|
|
737
|
+
|
|
738
|
+
// The person's messages a save was made from, in order: OpenViking's copy carries its own ids and the time it was
|
|
739
|
+
// captured, a little after the message was sent.
|
|
740
|
+
async function savedPrompts(ov, archive) {
|
|
741
|
+
try {
|
|
742
|
+
const text = await ovRead(ov, `/content/read?uri=${encodeURIComponent(`${archive}/messages.jsonl`)}`);
|
|
743
|
+
return String(typeof text === "string" ? text : text?.content ?? "")
|
|
744
|
+
.split("\n")
|
|
745
|
+
.map((line) => {
|
|
746
|
+
try {
|
|
747
|
+
return JSON.parse(line);
|
|
748
|
+
} catch {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
})
|
|
752
|
+
.filter((row) => row?.role === "user")
|
|
753
|
+
.map((row) => ({ key: promptKey((row.parts ?? []).filter((part) => part.type === "text").map((part) => part.text).join("\n")), at: Date.parse(row.created_at ?? "") || 0 }))
|
|
754
|
+
.filter((row) => row.key && !row.key.startsWith("<"));
|
|
755
|
+
} catch {
|
|
756
|
+
return [];
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// The thread's row for a message OpenViking captured: the latest prompt with the same text sent before the capture.
|
|
761
|
+
function rowFor(prompts, key, capturedAt) {
|
|
762
|
+
let found;
|
|
763
|
+
for (const prompt of prompts) if (prompt.keys.includes(key) && (!capturedAt || prompt.at <= capturedAt + 5000)) found = prompt;
|
|
764
|
+
return found;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// A new memory quotes the message it came from in its ChatLog: `**user [peer=…]**: <the message>`.
|
|
768
|
+
const quotedPrompt = (after) => promptKey(/\*\*user[^*]*\*\*:\s*(.+)/.exec(String(after ?? ""))?.[1] ?? "");
|
|
769
|
+
|
|
770
|
+
// Where memory went into the thread, as the transcript keeps it: what a hook added at the session's start or with
|
|
771
|
+
// a prompt (OpenViking's <openviking-context>, Peer's <teammates-project-knowledge>), and the agent's own calls of
|
|
772
|
+
// OpenViking's tools. Each under the person's message it came with, where there is one.
|
|
773
|
+
function threadUses(session, prompts) {
|
|
774
|
+
const projects = path.join(CLAUDE, "projects");
|
|
775
|
+
let file;
|
|
776
|
+
try {
|
|
777
|
+
file = fs.readdirSync(projects).map((dir) => path.join(projects, dir, `${session}.jsonl`)).find((f) => fs.existsSync(f));
|
|
778
|
+
} catch {}
|
|
779
|
+
if (!file) return [];
|
|
780
|
+
const byId = new Map(prompts.map((prompt) => [prompt.id, prompt]));
|
|
781
|
+
const name = (text) => path.basename(String(text).trim(), ".md").replace(/[_-]+/g, " ");
|
|
782
|
+
const clean = (text) => plain(String(text).replace(/\*\*/g, "").replace(/`/g, "")).slice(0, 120);
|
|
783
|
+
const uses = [];
|
|
784
|
+
let last;
|
|
785
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
786
|
+
let row;
|
|
787
|
+
try {
|
|
788
|
+
row = JSON.parse(line);
|
|
789
|
+
} catch {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (byId.has(row.uuid)) last = byId.get(row.uuid);
|
|
793
|
+
const at = row.timestamp ?? "";
|
|
794
|
+
const near = (where) => (where === "start" || where === "resume" || !last ? {} : { messageId: last.id, messageText: last.find?.text, messageMatch: last.find?.match });
|
|
795
|
+
const attachment = row.attachment;
|
|
796
|
+
if (row.type === "attachment" && attachment?.type === "hook_additional_context") {
|
|
797
|
+
const text = (attachment.content ?? []).join("\n");
|
|
798
|
+
const event = String(attachment.hookEvent ?? attachment.hookName ?? "");
|
|
799
|
+
for (const block of text.matchAll(/<openviking-context([^>]*)>([\s\S]*?)<\/openviking-context>/g)) {
|
|
800
|
+
const source = /source="(\w+)"/.exec(block[1])?.[1];
|
|
801
|
+
const where = event.startsWith("SessionStart") ? (source === "resume" ? "resume" : "start") : "prompt";
|
|
802
|
+
const listed = [...block[2].matchAll(/^\s+- (.+\.md)\s*$/gm)].map((m) => name(m[1]));
|
|
803
|
+
const recalled = [...block[2].matchAll(/^- \[\S+ \d+%\] (.+)$/gm)].map((m) => (m[1].startsWith("viking://") ? name(m[1]) : clean(m[1])));
|
|
804
|
+
const items = recalled.length ? recalled : listed;
|
|
805
|
+
if (items.length) uses.push({ at, where, from: "yours", kind: recalled.length ? "recalled" : "listed", items, ...near(where) });
|
|
806
|
+
}
|
|
807
|
+
for (const block of text.matchAll(/<teammates-project-knowledge[^>]*>([\s\S]*?)<\/teammates-project-knowledge>/g)) {
|
|
808
|
+
const items = [...block[1].matchAll(/^- \[[^\]]+\] (.+)$/gm)].map((m) => clean(m[1]));
|
|
809
|
+
const where = event.startsWith("SessionStart") ? "start" : "prompt";
|
|
810
|
+
if (items.length) uses.push({ at, where, from: "team", kind: "recalled", items, ...near(where) });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// The agent asking OpenViking itself, through its MCP tools (search, find, read, ...).
|
|
814
|
+
if (row.type === "assistant" && Array.isArray(row.message?.content)) {
|
|
815
|
+
for (const block of row.message.content) {
|
|
816
|
+
if (block?.type !== "tool_use" || !/openviking/i.test(String(block.name))) continue;
|
|
817
|
+
const input = block.input ?? {};
|
|
818
|
+
const detail = input.query ?? input.pattern ?? input.uri ?? (Array.isArray(input.uris) ? input.uris.join(", ") : "") ?? "";
|
|
819
|
+
uses.push({ at, where: "tool", from: "yours", kind: String(block.name).split("__").pop(), items: detail ? [clean(String(detail).startsWith("viking://") ? name(detail) : detail)] : [], ...near("tool") });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return uses.slice(-200);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async function threadMemories(session) {
|
|
827
|
+
const ov = readJson(OV_CONF);
|
|
828
|
+
if (!ov?.url) return { connected: false, reason: `no OpenViking client config at ${OV_CONF}` };
|
|
829
|
+
const base = `viking://user/${ov.user}/sessions/cc-${session}`;
|
|
830
|
+
let archives;
|
|
831
|
+
try {
|
|
832
|
+
archives = await ovRead(ov, `/fs/ls?uri=${encodeURIComponent(`${base}/history`)}`);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
if (/not found|no such|404/i.test(error.message)) return { connected: true, saves: [], uses: threadUses(session, transcriptPrompts(session)) };
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
const saves = [];
|
|
838
|
+
const prompts = transcriptPrompts(session);
|
|
839
|
+
for (const archive of archives.filter((entry) => entry.isDir)) {
|
|
840
|
+
let diff;
|
|
841
|
+
try {
|
|
842
|
+
diff = JSON.parse(await ovRead(ov, `/content/read?uri=${encodeURIComponent(`${archive.uri}/memory_diff.json`)}`));
|
|
843
|
+
} catch {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const ops = diff.operations ?? {};
|
|
847
|
+
// Which row in the thread each memory belongs to: a new one under the message it quotes (exact); an update or
|
|
848
|
+
// a removal, which quotes nothing, under the save's last message (only near it).
|
|
849
|
+
const captured = prompts.length ? await savedPrompts(ov, archive.uri) : [];
|
|
850
|
+
// An update goes under the save's last message that is the person's own (a skill's or command's text is not).
|
|
851
|
+
let nearRow;
|
|
852
|
+
for (const row of [...captured].reverse()) if ((nearRow = rowFor(prompts, row.key, row.at))) break;
|
|
853
|
+
const quotedRow = (after) => {
|
|
854
|
+
const key = quotedPrompt(after);
|
|
855
|
+
const copy = [...captured].reverse().find((row) => row.key === key);
|
|
856
|
+
return key ? rowFor(prompts, key, copy?.at) : undefined;
|
|
857
|
+
};
|
|
858
|
+
// The row's id, and its words as typed: where a surface cannot scroll to the row, its find can look for them.
|
|
859
|
+
const placed = (row, exact) =>
|
|
860
|
+
row ? { messageId: row.id, messageText: row.find.text, messageMatch: row.find.match, exact } : nearRow ? { messageId: nearRow.id, messageText: nearRow.find.text, messageMatch: nearRow.find.match, exact: false } : {};
|
|
861
|
+
const items = [
|
|
862
|
+
...(ops.adds ?? []).map((op) => ({ change: "added", type: op.memory_type, name: memoryName(op.uri), lines: [summaryOf(op.after)], systemText: hasSystemText(op.after), ...placed(quotedRow(op.after), true) })),
|
|
863
|
+
...(ops.updates ?? []).map((op) => ({ change: "updated", type: op.memory_type, name: memoryName(op.uri), lines: addedLines(op.before, op.after), systemText: hasSystemText(op.after) && !hasSystemText(op.before), ...placed(undefined, false) })),
|
|
864
|
+
...(ops.deletes ?? []).map((op) => ({ change: "removed", type: op.memory_type, name: memoryName(op.uri), lines: [summaryOf(op.before)], systemText: false, ...placed(undefined, false) })),
|
|
865
|
+
].map((item) => ({ ...item, name: plain(item.name), lines: item.lines.filter(Boolean).slice(0, 3).map((line) => String(line).replace(/[\u0000-\u001f\u007f-\u009f]/g, "").slice(0, 300)) }));
|
|
866
|
+
// Each memory's text for a card: a new or removed one whole, an update as the lines it added.
|
|
867
|
+
const bodies = [
|
|
868
|
+
...(ops.adds ?? []).map((op) => memoryBody(op.after, memoryName(op.uri))),
|
|
869
|
+
...(ops.updates ?? []).map((op) => addedLines(op.before, op.after).slice(0, 12).map((line) => `- ${line}`).join("\n")),
|
|
870
|
+
...(ops.deletes ?? []).map((op) => memoryBody(op.before, memoryName(op.uri))),
|
|
871
|
+
];
|
|
872
|
+
items.forEach((item, index) => (item.body = String(bodies[index] ?? "").slice(0, 3000)));
|
|
873
|
+
saves.push({ at: diff.extracted_at ?? archive.modTime, items });
|
|
874
|
+
}
|
|
875
|
+
return { connected: true, saves: saves.sort((a, b) => String(b.at).localeCompare(String(a.at))), uses: threadUses(session, prompts) };
|
|
876
|
+
}
|
|
877
|
+
|
|
579
878
|
// ---- team memory: teammates' published project knowledge at the start of a thread ----
|
|
580
879
|
// The team's OpenViking server holds pages members published for a project. `peer team connect` saves this
|
|
581
880
|
// member's address and key; the hooks then add the best few teammates' pages to the first prompt of a thread
|
|
@@ -587,7 +886,10 @@ const TEAM_LOG = path.join(HOME, "team-memory", "recall.log");
|
|
|
587
886
|
|
|
588
887
|
// OpenViking names a repository by its normalized origin URL; the team scripts do the same.
|
|
589
888
|
function repositoryPeer(cwd) {
|
|
590
|
-
|
|
889
|
+
return repositoryKey(git(cwd, "remote", "get-url", "origin"));
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function repositoryKey(remote) {
|
|
591
893
|
if (!remote) return "";
|
|
592
894
|
return remote
|
|
593
895
|
.replace(/^[a-z+]+:\/\//i, "")
|
|
@@ -610,9 +912,19 @@ async function teamCall(team, method, route, body, timeoutMs = 3000) {
|
|
|
610
912
|
return { status: response.status, value: await response.json().catch(() => ({})) };
|
|
611
913
|
}
|
|
612
914
|
|
|
915
|
+
// The project is the Hub's: the workspace project whose repositories include this one, with all of its
|
|
916
|
+
// repositories. A repository in no Hub project (or without a sign-in) is a project of its own, named by its key.
|
|
917
|
+
async function teamProject(repo) {
|
|
918
|
+
const saved = config();
|
|
919
|
+
if (!saved?.session || !saved?.workspace) return { project: repo, repos: [repo] };
|
|
920
|
+
const manifest = await call(saved, "GET", `/workspaces/${saved.workspace}/manifest`, undefined, 10000);
|
|
921
|
+
const keysOf = (project) => (project.repositories ?? []).map((r) => repositoryKey(r.url ?? "")).filter(Boolean);
|
|
922
|
+
const found = (manifest?.projects ?? []).find((project) => keysOf(project).includes(repo));
|
|
923
|
+
return found ? { project: found.id, repos: keysOf(found) } : { project: repo, repos: [repo] };
|
|
924
|
+
}
|
|
925
|
+
|
|
613
926
|
async function teamConnect(args) {
|
|
614
927
|
const member = option(args, "--as");
|
|
615
|
-
const project = option(args, "--project") ?? "alpha";
|
|
616
928
|
const repo = option(args, "--repo") ?? repositoryPeer(process.cwd());
|
|
617
929
|
const url = (args[0] || "").replace(/\/+$/, "");
|
|
618
930
|
if (!url || !member) throw new Error("usage: peer team connect <server-url> --as <member> [--project <id>] [--repo <id>], key on stdin");
|
|
@@ -622,11 +934,14 @@ async function teamConnect(args) {
|
|
|
622
934
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
623
935
|
const key = Buffer.concat(chunks).toString("utf8").trim();
|
|
624
936
|
if (!key) throw new Error("no key on stdin: printf '%s' \"$KEY\" | peer team connect …");
|
|
937
|
+
const given = option(args, "--project");
|
|
938
|
+
const found = given ? { project: given, repos: [repo] } : await teamProject(repo);
|
|
939
|
+
const { project } = found;
|
|
625
940
|
const team = { url, member, key };
|
|
626
941
|
const { status } = await teamCall(team, "POST", "/search/find", { query: "probe", target_uri: `viking://resources/projects/${project}`, limit: 1 }, 10000);
|
|
627
942
|
if (status >= 300) throw new Error(`the team server refused ${member}'s key for project ${project} (${status})`);
|
|
628
943
|
const saved = readJson(TEAM_FILE) ?? {};
|
|
629
|
-
const repos = saved.project === project ?
|
|
944
|
+
const repos = [...new Set([...(saved.project === project ? saved.repos ?? [] : []), ...found.repos, repo])];
|
|
630
945
|
writeJson(TEAM_FILE, { url, member, project, repos, key });
|
|
631
946
|
console.log(`Team memory: ${member} on ${url}, project ${project}, repositories ${repos.join(", ")}.`);
|
|
632
947
|
if (readJson(SETTINGS)) install();
|
|
@@ -721,6 +1036,159 @@ async function teamHook(event) {
|
|
|
721
1036
|
answer({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: block } });
|
|
722
1037
|
}
|
|
723
1038
|
|
|
1039
|
+
// ---- your own memory: what your personal OpenViking remembered, listed and searched ----
|
|
1040
|
+
// Reads the person's own OpenViking server as its CLI is configured (~/.openviking/ovcli.conf). `/peer-all` is
|
|
1041
|
+
// answered by the hook, so the list is shown to the person and never enters the thread (nor its capture).
|
|
1042
|
+
|
|
1043
|
+
const MEMORY_CONF = process.env.PEER_OPENVIKING_CONF || path.join(os.homedir(), ".openviking", "ovcli.conf");
|
|
1044
|
+
const MEMORY_PAGE = 20;
|
|
1045
|
+
|
|
1046
|
+
function memoryServer() {
|
|
1047
|
+
const conf = readJson(MEMORY_CONF);
|
|
1048
|
+
if (!conf?.url || !conf?.user) throw new Error(`no personal OpenViking configured (${MEMORY_CONF})`);
|
|
1049
|
+
return conf;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function memoryCall(conf, route, body) {
|
|
1053
|
+
const response = await fetch(`${conf.url.replace(/\/+$/, "")}/api/v1${route}`, {
|
|
1054
|
+
method: "POST",
|
|
1055
|
+
headers: {
|
|
1056
|
+
"content-type": "application/json",
|
|
1057
|
+
...(conf.api_key ? { "x-api-key": conf.api_key } : {}),
|
|
1058
|
+
"x-openviking-account": conf.account || "default",
|
|
1059
|
+
"x-openviking-user": conf.user,
|
|
1060
|
+
},
|
|
1061
|
+
body: JSON.stringify(body),
|
|
1062
|
+
signal: AbortSignal.timeout(8000),
|
|
1063
|
+
});
|
|
1064
|
+
const value = await response.json().catch(() => ({}));
|
|
1065
|
+
if (!response.ok) throw new Error(`${route}: ${response.status} ${plain(value?.error?.message ?? value?.detail ?? "")}`);
|
|
1066
|
+
return value.result ?? {};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// viking://user/<u>/peers/<repo>/memories/<type>/… or viking://user/<u>/memories/<type>/… (global).
|
|
1070
|
+
function memoryWhere(uri) {
|
|
1071
|
+
const match = /^viking:\/\/user\/[^/]+\/(?:peers\/([^/]+)\/)?memories\/([^/]+)\/(.+)$/.exec(uri);
|
|
1072
|
+
if (!match) return null;
|
|
1073
|
+
const [, repo, type, rest] = match;
|
|
1074
|
+
const title = rest.split("/").pop().replace(/\.md$/, "").replace(/[_-]+/g, " ");
|
|
1075
|
+
return { repo: repo || "global", type, title };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const isMemoryFile = (uri) => uri.endsWith(".md") && !uri.split("/").pop().startsWith(".") && memoryWhere(uri);
|
|
1079
|
+
|
|
1080
|
+
// `peer all [text] [page N] [--here] [--newest | --oldest]`; the same words after /peer-all. Without a sort the
|
|
1081
|
+
// memories come in OpenViking's own order.
|
|
1082
|
+
function memoryQuery(words, cwd) {
|
|
1083
|
+
const query = { text: [], page: 1, here: false, sort: "" };
|
|
1084
|
+
for (let i = 0; i < words.length; i++) {
|
|
1085
|
+
if (words[i] === "--here" || words[i] === "here") query.here = true;
|
|
1086
|
+
else if (words[i] === "--newest" || words[i] === "--oldest") query.sort = words[i] === "--newest" ? "new" : "old";
|
|
1087
|
+
else if ((words[i] === "page" || words[i] === "--page") && /^\d+$/.test(words[i + 1] ?? "")) query.page = Math.max(1, Number(words[++i]));
|
|
1088
|
+
else query.text.push(words[i]);
|
|
1089
|
+
}
|
|
1090
|
+
query.text = query.text.join(" ").trim();
|
|
1091
|
+
query.repo = query.here ? repositoryPeer(cwd) : "";
|
|
1092
|
+
return query;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Diacritics do not count: a letter matches its accented forms, so `pamat` finds "pamäť" and `pamäť` finds "pamat".
|
|
1096
|
+
const LETTERS = { a: "aáäàâ", c: "cčç", d: "dď", e: "eéěëèê", i: "iíìî", l: "lĺľ", n: "nňñ", o: "oóôöò", r: "rŕř", s: "sš", t: "tť", u: "uúůüù", y: "yý", z: "zž" };
|
|
1097
|
+
|
|
1098
|
+
// The words of a search as OpenViking grep patterns: each word, or each "quoted phrase" as one, at most six.
|
|
1099
|
+
function memoryTerms(text) {
|
|
1100
|
+
const words = [...text.matchAll(/"([^"]+)"|(\S+)/g)].map((m) => (m[1] ?? m[2]).trim()).filter(Boolean).slice(0, 6);
|
|
1101
|
+
return words.map((word) =>
|
|
1102
|
+
[...word.toLowerCase()]
|
|
1103
|
+
.map((char) => {
|
|
1104
|
+
const base = char.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
1105
|
+
if (LETTERS[base]) return `[${LETTERS[base]}]`;
|
|
1106
|
+
return /\s/.test(char) ? "\\s+" : char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1107
|
+
})
|
|
1108
|
+
.join(""),
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// One page of the person's memories: all of them, or the files whose text has the words; each with the time it last
|
|
1113
|
+
// changed (OpenViking keeps no other time per file), which is when an event was added.
|
|
1114
|
+
async function memoryPage(query) {
|
|
1115
|
+
const conf = memoryServer();
|
|
1116
|
+
const root = `viking://user/${conf.user}`;
|
|
1117
|
+
const scope = query.repo ? `${root}/peers/${query.repo}/memories` : root;
|
|
1118
|
+
// The times come from the listing, so a search asks for both at once.
|
|
1119
|
+
const listing = memoryCall(conf, "/search/glob", { pattern: "**/*.md", uri: scope, node_limit: 20000, extra_fields: ["modTime"] });
|
|
1120
|
+
let rows;
|
|
1121
|
+
if (query.text) {
|
|
1122
|
+
// Every word (or "quoted phrase") must be somewhere in the memory, in any order: one grep each, then the files
|
|
1123
|
+
// all of them hit. Lines are matched, so the words of one memory may sit on different lines.
|
|
1124
|
+
const terms = memoryTerms(query.text);
|
|
1125
|
+
const [files, ...found] = await Promise.all([
|
|
1126
|
+
listing,
|
|
1127
|
+
...terms.map((term) =>
|
|
1128
|
+
memoryCall(conf, "/search/grep", { uri: scope, exclude_uri: `${root}/sessions`, pattern: term, case_insensitive: true, node_limit: 2000 }),
|
|
1129
|
+
),
|
|
1130
|
+
]);
|
|
1131
|
+
const times = new Map((files.matches ?? []).map((m) => [m.uri, m.modTime]));
|
|
1132
|
+
const byFile = new Map();
|
|
1133
|
+
found.forEach((result, index) => {
|
|
1134
|
+
for (const hit of result.matches ?? []) {
|
|
1135
|
+
if (!isMemoryFile(hit.uri)) continue;
|
|
1136
|
+
if (!byFile.has(hit.uri)) byFile.set(hit.uri, { uri: hit.uri, at: times.get(hit.uri), lines: [], hits: new Set() });
|
|
1137
|
+
const file = byFile.get(hit.uri);
|
|
1138
|
+
file.hits.add(index);
|
|
1139
|
+
const line = String(hit.content ?? "").trim();
|
|
1140
|
+
if (!file.lines.includes(line)) file.lines.push(line);
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
rows = [...byFile.values()].filter((file) => file.hits.size === terms.length);
|
|
1144
|
+
} else {
|
|
1145
|
+
const found = await listing;
|
|
1146
|
+
rows = (found.matches ?? []).filter((m) => !m.isDir && isMemoryFile(m.uri)).map((m) => ({ uri: m.uri, at: m.modTime }));
|
|
1147
|
+
}
|
|
1148
|
+
if (query.sort) rows.sort((a, b) => String(query.sort === "new" ? b.at : a.at).localeCompare(String(query.sort === "new" ? a.at : b.at)));
|
|
1149
|
+
const pages = Math.max(1, Math.ceil(rows.length / MEMORY_PAGE));
|
|
1150
|
+
const page = Math.min(query.page, pages);
|
|
1151
|
+
const clean = (text, max) => String(text ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, max);
|
|
1152
|
+
return {
|
|
1153
|
+
connected: true,
|
|
1154
|
+
url: conf.url.replace(/\/+$/, ""),
|
|
1155
|
+
text: query.text,
|
|
1156
|
+
repo: query.repo,
|
|
1157
|
+
sort: query.sort,
|
|
1158
|
+
total: rows.length,
|
|
1159
|
+
page,
|
|
1160
|
+
pages,
|
|
1161
|
+
rows: rows.slice((page - 1) * MEMORY_PAGE, page * MEMORY_PAGE).map((row) => {
|
|
1162
|
+
const where = memoryWhere(row.uri);
|
|
1163
|
+
return {
|
|
1164
|
+
uri: clean(row.uri, 400),
|
|
1165
|
+
repo: plain(where.repo),
|
|
1166
|
+
type: plain(where.type),
|
|
1167
|
+
title: plain(where.title),
|
|
1168
|
+
at: row.at ? String(row.at).slice(0, 16).replace("T", " ") : "",
|
|
1169
|
+
lines: (row.lines ?? []).slice(0, 2).map((line) => clean(line, 160)),
|
|
1170
|
+
};
|
|
1171
|
+
}),
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async function memoryText(query) {
|
|
1176
|
+
const found = await memoryPage(query);
|
|
1177
|
+
const { page, pages } = found;
|
|
1178
|
+
const order = { new: "newest first", old: "oldest first" }[query.sort];
|
|
1179
|
+
const what = [query.text ? `"${plain(query.text)}"` : "all", query.repo ? `in ${query.repo}` : "", order].filter(Boolean).join(" ");
|
|
1180
|
+
const lines = [`Your memories (${found.url}), ${what}: ${found.total} file(s), page ${page} of ${pages}`];
|
|
1181
|
+
for (const row of found.rows) {
|
|
1182
|
+
lines.push("", `${row.at ? `${row.at} ` : ""}${row.repo} · ${row.type} · ${row.title}`);
|
|
1183
|
+
for (const line of row.lines) lines.push(` ${line}`);
|
|
1184
|
+
lines.push(` ${row.uri}`);
|
|
1185
|
+
}
|
|
1186
|
+
if (!found.total) lines.push("", "Nothing found.");
|
|
1187
|
+
const next = [query.text, query.here ? "here" : "", query.sort ? `--${query.sort === "new" ? "newest" : "oldest"}` : "", page < pages ? `page ${page + 1}` : ""].filter(Boolean).join(" ");
|
|
1188
|
+
lines.push("", page < pages ? `Next: /peer-all ${next}` : "", `Search: /peer-all <words> · this repository only: /peer-all here <words> · by time: --newest or --oldest · edit or delete: OpenViking Studio, ${found.url}/studio/`);
|
|
1189
|
+
return lines.filter((line, i, all) => line || all[i - 1]).join("\n").trim();
|
|
1190
|
+
}
|
|
1191
|
+
|
|
724
1192
|
// ---- main ----
|
|
725
1193
|
|
|
726
1194
|
const [command, ...args] = process.argv.slice(2);
|
|
@@ -733,8 +1201,28 @@ try {
|
|
|
733
1201
|
await logout();
|
|
734
1202
|
break;
|
|
735
1203
|
case "list":
|
|
736
|
-
|
|
1204
|
+
// `--json` is for the thread-mark mod's pane: the hub's list as it came, plus who is asking.
|
|
1205
|
+
if (args.includes("--json")) {
|
|
1206
|
+
const saved = signedIn();
|
|
1207
|
+
const list = await call(saved, "GET", `/workspaces/${saved.workspace}/agents`);
|
|
1208
|
+
console.log(JSON.stringify({ ...list, you: saved.email }));
|
|
1209
|
+
} else console.log(await listText(signedIn()));
|
|
1210
|
+
break;
|
|
1211
|
+
case "thread": {
|
|
1212
|
+
// `peer thread --json <session id>` is for the thread-mark mod's pane.
|
|
1213
|
+
const session = args.find((arg) => !arg.startsWith("--"));
|
|
1214
|
+
if (!validSession(session)) throw new Error("usage: peer thread [--json] <claude code session id>");
|
|
1215
|
+
const memories = await threadMemories(session);
|
|
1216
|
+
if (args.includes("--json")) console.log(JSON.stringify(memories));
|
|
1217
|
+
else if (!memories.connected) console.log(`Peer: ${memories.reason}`);
|
|
1218
|
+
else if (!memories.saves.length) console.log("This thread has not saved anything to memory yet.");
|
|
1219
|
+
else
|
|
1220
|
+
for (const save of memories.saves) {
|
|
1221
|
+
console.log(save.at);
|
|
1222
|
+
for (const item of save.items) console.log(` ${item.change} ${item.type} "${item.name}": ${item.lines.join(" / ")}${item.systemText ? " [holds Claude Code's own text]" : ""}`);
|
|
1223
|
+
}
|
|
737
1224
|
break;
|
|
1225
|
+
}
|
|
738
1226
|
case "status":
|
|
739
1227
|
status();
|
|
740
1228
|
break;
|
|
@@ -752,6 +1240,17 @@ try {
|
|
|
752
1240
|
// A hook never breaks the person's session: whatever goes wrong, it exits 0.
|
|
753
1241
|
await (args[0]?.startsWith("team-") ? teamHook(args[0]) : hook(args[0])).catch((error) => log(`hook ${args[0]} failed: ${error?.message ?? error}`));
|
|
754
1242
|
break;
|
|
1243
|
+
case "all": {
|
|
1244
|
+
// `peer all --json …` is for the thread-mark mod's pane; without a personal OpenViking it says so.
|
|
1245
|
+
const json = args.includes("--json");
|
|
1246
|
+
const query = memoryQuery(args.filter((arg) => arg !== "--json"), process.cwd());
|
|
1247
|
+
if (!json) console.log(await memoryText(query));
|
|
1248
|
+
else {
|
|
1249
|
+
const found = await memoryPage(query).catch((error) => ({ connected: false, reason: error.message }));
|
|
1250
|
+
console.log(JSON.stringify(found));
|
|
1251
|
+
}
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
755
1254
|
case "team":
|
|
756
1255
|
if (args[0] === "connect") await teamConnect(args.slice(1));
|
|
757
1256
|
else if (args[0] === "status") teamStatus();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peer-thread-mark",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Marks a Claude Code thread shared with Peer (/peer) with a band above the prompt, with /peer stop and /peer start",
|
|
3
|
+
"version": "0.4.14",
|
|
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"
|
|
7
7
|
}
|