@byterover/claude-plugin 1.0.0
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/LICENSE +44 -0
- package/README.md +304 -0
- package/dist/bridge-command.d.ts +23 -0
- package/dist/bridge-command.js +103 -0
- package/dist/cc-frontmatter.d.ts +21 -0
- package/dist/cc-frontmatter.js +51 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +20 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +150 -0
- package/dist/commands/ingest.d.ts +2 -0
- package/dist/commands/ingest.js +101 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +130 -0
- package/dist/commands/recall.d.ts +2 -0
- package/dist/commands/recall.js +41 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +122 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +72 -0
- package/dist/memory-path.d.ts +16 -0
- package/dist/memory-path.js +214 -0
- package/dist/schemas/cc-hook-input.d.ts +129 -0
- package/dist/schemas/cc-hook-input.js +30 -0
- package/dist/schemas/cc-settings.d.ts +24 -0
- package/dist/schemas/cc-settings.js +35 -0
- package/dist/stdin.d.ts +7 -0
- package/dist/stdin.js +24 -0
- package/package.json +62 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { isBridgeHook, resolveBridgeExecutable } from "../bridge-command.js";
|
|
6
|
+
import { getCcMemoryDir, getClaudeConfigHome } from "../memory-path.js";
|
|
7
|
+
import { readSettingsRaw } from "../schemas/cc-settings.js";
|
|
8
|
+
export function registerDoctorCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("doctor")
|
|
11
|
+
.description("Check that the bridge is correctly configured")
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const results = [];
|
|
15
|
+
// 1. brv CLI found
|
|
16
|
+
try {
|
|
17
|
+
const version = execSync("brv --version", {
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
timeout: 5_000,
|
|
20
|
+
}).trim();
|
|
21
|
+
results.push({ label: "brv CLI", pass: true, detail: version });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
results.push({
|
|
25
|
+
label: "brv CLI",
|
|
26
|
+
pass: false,
|
|
27
|
+
detail: "not found in PATH",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// 2. .brv/context-tree/ exists
|
|
31
|
+
const ctPath = join(cwd, ".brv", "context-tree");
|
|
32
|
+
results.push({
|
|
33
|
+
label: "Context tree",
|
|
34
|
+
pass: existsSync(ctPath),
|
|
35
|
+
detail: existsSync(ctPath) ? ctPath : `${ctPath} not found`,
|
|
36
|
+
});
|
|
37
|
+
// 3. Settings file exists and is valid JSON
|
|
38
|
+
const settingsPath = join(getClaudeConfigHome(), "settings.json");
|
|
39
|
+
let settingsValid = false;
|
|
40
|
+
try {
|
|
41
|
+
readSettingsRaw(settingsPath);
|
|
42
|
+
settingsValid = existsSync(settingsPath);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
settingsValid = false;
|
|
46
|
+
}
|
|
47
|
+
results.push({
|
|
48
|
+
label: "Claude settings",
|
|
49
|
+
pass: settingsValid,
|
|
50
|
+
detail: settingsValid ? settingsPath : `${settingsPath} missing or invalid`,
|
|
51
|
+
});
|
|
52
|
+
// 4. Bridge hooks installed
|
|
53
|
+
if (settingsValid) {
|
|
54
|
+
const settings = readSettingsRaw(settingsPath);
|
|
55
|
+
const hooks = settings.hooks;
|
|
56
|
+
const hasPostToolUse = findBridgeHookInEvent(hooks, "PostToolUse");
|
|
57
|
+
const hasStop = findBridgeHookInEvent(hooks, "Stop");
|
|
58
|
+
const hasUserPrompt = findBridgeHookInEvent(hooks, "UserPromptSubmit");
|
|
59
|
+
const installed = hasPostToolUse && hasStop && hasUserPrompt;
|
|
60
|
+
results.push({
|
|
61
|
+
label: "Bridge hooks",
|
|
62
|
+
pass: installed,
|
|
63
|
+
detail: installed
|
|
64
|
+
? "PostToolUse + Stop + UserPromptSubmit hooks found"
|
|
65
|
+
: `missing: ${[
|
|
66
|
+
!hasPostToolUse && "PostToolUse",
|
|
67
|
+
!hasStop && "Stop",
|
|
68
|
+
!hasUserPrompt && "UserPromptSubmit",
|
|
69
|
+
]
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join(", ")}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
results.push({
|
|
76
|
+
label: "Bridge hooks",
|
|
77
|
+
pass: false,
|
|
78
|
+
detail: "cannot check — settings invalid",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// 5. Bridge executable resolves
|
|
82
|
+
try {
|
|
83
|
+
const exe = resolveBridgeExecutable();
|
|
84
|
+
results.push({
|
|
85
|
+
label: "Bridge executable",
|
|
86
|
+
pass: true,
|
|
87
|
+
detail: exe,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
results.push({
|
|
92
|
+
label: "Bridge executable",
|
|
93
|
+
pass: false,
|
|
94
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// 6. cc memory dir resolves
|
|
98
|
+
try {
|
|
99
|
+
const memDir = getCcMemoryDir(cwd);
|
|
100
|
+
const memExists = existsSync(memDir);
|
|
101
|
+
results.push({
|
|
102
|
+
label: "Memory directory",
|
|
103
|
+
pass: memExists,
|
|
104
|
+
detail: memExists
|
|
105
|
+
? memDir
|
|
106
|
+
: `${memDir} (resolved but does not exist yet — will be created on first Claude session)`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
results.push({
|
|
111
|
+
label: "Memory directory",
|
|
112
|
+
pass: false,
|
|
113
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Print results
|
|
117
|
+
console.log(pc.bold("\nbrv-claude-plugin doctor\n"));
|
|
118
|
+
let allPass = true;
|
|
119
|
+
for (const r of results) {
|
|
120
|
+
const icon = r.pass ? pc.green("\u2713") : pc.red("\u2717");
|
|
121
|
+
const detail = r.detail ? pc.dim(` — ${r.detail}`) : "";
|
|
122
|
+
console.log(` ${icon} ${r.label}${detail}`);
|
|
123
|
+
if (!r.pass)
|
|
124
|
+
allPass = false;
|
|
125
|
+
}
|
|
126
|
+
console.log();
|
|
127
|
+
if (allPass) {
|
|
128
|
+
console.log(pc.green("All checks passed."));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(pc.yellow("Some checks failed. Run 'brv-claude-plugin install' to set up."));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function findBridgeHookInEvent(hooks, event) {
|
|
136
|
+
if (!hooks)
|
|
137
|
+
return false;
|
|
138
|
+
const eventHooks = hooks[event];
|
|
139
|
+
if (!Array.isArray(eventHooks))
|
|
140
|
+
return false;
|
|
141
|
+
return eventHooks.some((entry) => {
|
|
142
|
+
const innerHooks = entry.hooks;
|
|
143
|
+
if (!Array.isArray(innerHooks))
|
|
144
|
+
return false;
|
|
145
|
+
return innerHooks.some((h) => typeof h === "object" &&
|
|
146
|
+
h !== null &&
|
|
147
|
+
isBridgeHook(h));
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { BrvBridge } from "@byterover/brv-bridge";
|
|
4
|
+
import { parseCcMemoryFile } from "../cc-frontmatter.js";
|
|
5
|
+
import { getCcMemoryDir, isCcMemoryPath } from "../memory-path.js";
|
|
6
|
+
import { PostToolUseHookInputSchema } from "../schemas/cc-hook-input.js";
|
|
7
|
+
import { readStdinJson } from "../stdin.js";
|
|
8
|
+
// Files in the memory dir that we should never ingest
|
|
9
|
+
const SKIP_FILES = new Set(["MEMORY.md", "_brv_context.md"]);
|
|
10
|
+
export function registerIngestCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command("ingest")
|
|
13
|
+
.description("Ingest a Claude Code memory file into ByteRover context tree (called by PostToolUse hook)")
|
|
14
|
+
.option("--json", "Output result as JSON")
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
try {
|
|
17
|
+
const input = await readStdinJson(PostToolUseHookInputSchema);
|
|
18
|
+
const toolInput = input.tool_input;
|
|
19
|
+
const filePath = toolInput.file_path;
|
|
20
|
+
const toolName = input.tool_name;
|
|
21
|
+
const cwd = input.cwd;
|
|
22
|
+
if (!filePath || typeof filePath !== "string") {
|
|
23
|
+
exit(opts, { ingested: false, reason: "no file_path in tool_input" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Guard: only process files in cc memory directory
|
|
27
|
+
const memoryDir = getCcMemoryDir(cwd);
|
|
28
|
+
if (!isCcMemoryPath(filePath, memoryDir)) {
|
|
29
|
+
exit(opts, { ingested: false, reason: "not a memory path" });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Guard: skip index and our own file
|
|
33
|
+
const fileName = basename(filePath);
|
|
34
|
+
if (SKIP_FILES.has(fileName)) {
|
|
35
|
+
exit(opts, { ingested: false, reason: `skipped ${fileName}` });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Get file content based on tool type
|
|
39
|
+
let content;
|
|
40
|
+
if (toolName === "Write") {
|
|
41
|
+
// Write tool: content is in tool_input
|
|
42
|
+
const rawContent = toolInput.content;
|
|
43
|
+
if (typeof rawContent !== "string") {
|
|
44
|
+
exit(opts, { ingested: false, reason: "no content in Write input" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
content = rawContent;
|
|
48
|
+
}
|
|
49
|
+
else if (toolName === "Edit") {
|
|
50
|
+
// Edit tool: only has old_string/new_string — read from disk
|
|
51
|
+
try {
|
|
52
|
+
content = readFileSync(filePath, "utf-8");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
exit(opts, {
|
|
56
|
+
ingested: false,
|
|
57
|
+
reason: `cannot read file after Edit: ${filePath}`,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
exit(opts, { ingested: false, reason: `unexpected tool: ${toolName}` });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Parse cc-ts frontmatter
|
|
67
|
+
const parsed = parseCcMemoryFile(content);
|
|
68
|
+
if (!parsed) {
|
|
69
|
+
exit(opts, {
|
|
70
|
+
ingested: false,
|
|
71
|
+
reason: "invalid cc-ts memory frontmatter",
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const { frontmatter, body } = parsed;
|
|
76
|
+
// Pass content to brv curate without path constraints.
|
|
77
|
+
// Let ByteRover's agent decide the best domain/topic structure.
|
|
78
|
+
const curateContext = body.trim();
|
|
79
|
+
// Fire-and-forget curate
|
|
80
|
+
const bridge = new BrvBridge({ cwd, persistTimeoutMs: 15_000 });
|
|
81
|
+
await bridge.persist(curateContext);
|
|
82
|
+
exit(opts, {
|
|
83
|
+
ingested: true,
|
|
84
|
+
name: frontmatter.name,
|
|
85
|
+
type: frontmatter.type,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
// All errors → stderr + exit 0. Never exit 2 (would block Claude).
|
|
90
|
+
process.stderr.write(`brv-claude-plugin ingest error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function exit(opts, result) {
|
|
96
|
+
if (opts.json) {
|
|
97
|
+
console.log(JSON.stringify(result));
|
|
98
|
+
}
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=ingest.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { buildHookCommand, isBridgeHook, resolveBridgeExecutable, } from "../bridge-command.js";
|
|
4
|
+
import { getClaudeConfigHome } from "../memory-path.js";
|
|
5
|
+
import { backupSettings, readSettingsRaw, writeSettingsRaw, } from "../schemas/cc-settings.js";
|
|
6
|
+
// The hook entries we want to install
|
|
7
|
+
function buildDesiredHooks() {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
event: "PostToolUse",
|
|
11
|
+
entry: {
|
|
12
|
+
matcher: "Write",
|
|
13
|
+
hooks: [
|
|
14
|
+
{
|
|
15
|
+
type: "command",
|
|
16
|
+
command: buildHookCommand("ingest"),
|
|
17
|
+
async: true,
|
|
18
|
+
timeout: 15,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
event: "PostToolUse",
|
|
25
|
+
entry: {
|
|
26
|
+
matcher: "Edit",
|
|
27
|
+
hooks: [
|
|
28
|
+
{
|
|
29
|
+
type: "command",
|
|
30
|
+
command: buildHookCommand("ingest"),
|
|
31
|
+
async: true,
|
|
32
|
+
timeout: 15,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
event: "Stop",
|
|
39
|
+
entry: {
|
|
40
|
+
hooks: [
|
|
41
|
+
{
|
|
42
|
+
type: "command",
|
|
43
|
+
command: buildHookCommand("sync"),
|
|
44
|
+
async: true,
|
|
45
|
+
timeout: 10,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
event: "UserPromptSubmit",
|
|
52
|
+
entry: {
|
|
53
|
+
hooks: [
|
|
54
|
+
{
|
|
55
|
+
type: "command",
|
|
56
|
+
command: buildHookCommand("recall"),
|
|
57
|
+
timeout: 8,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
export function registerInstallCommand(program) {
|
|
65
|
+
program
|
|
66
|
+
.command("install")
|
|
67
|
+
.description("Install Claude Code hooks that bridge auto-memory to ByteRover context tree")
|
|
68
|
+
.option("--dry-run", "Show what would be written without modifying files")
|
|
69
|
+
.option("--settings-path <path>", "Override path to Claude Code settings.json")
|
|
70
|
+
.action(async (opts) => {
|
|
71
|
+
try {
|
|
72
|
+
// Validate executable resolves before touching settings
|
|
73
|
+
const exe = resolveBridgeExecutable();
|
|
74
|
+
console.log(pc.dim(`Resolved executable: ${exe}`));
|
|
75
|
+
const settingsPath = opts.settingsPath ??
|
|
76
|
+
join(getClaudeConfigHome(), "settings.json");
|
|
77
|
+
const settings = readSettingsRaw(settingsPath);
|
|
78
|
+
// Ensure hooks object exists
|
|
79
|
+
if (!settings.hooks || typeof settings.hooks !== "object") {
|
|
80
|
+
settings.hooks = {};
|
|
81
|
+
}
|
|
82
|
+
const hooks = settings.hooks;
|
|
83
|
+
const desired = buildDesiredHooks();
|
|
84
|
+
let added = 0;
|
|
85
|
+
for (const { event, entry } of desired) {
|
|
86
|
+
if (!Array.isArray(hooks[event])) {
|
|
87
|
+
hooks[event] = [];
|
|
88
|
+
}
|
|
89
|
+
const eventHooks = hooks[event];
|
|
90
|
+
// Per-hook dedupe: check if any existing matcher entry with the
|
|
91
|
+
// same matcher value already contains a bridge hook
|
|
92
|
+
const matcherValue = "matcher" in entry ? entry.matcher : undefined;
|
|
93
|
+
const alreadyInstalled = eventHooks.some((existing) => {
|
|
94
|
+
const existingMatcher = existing.matcher;
|
|
95
|
+
if (existingMatcher !== matcherValue)
|
|
96
|
+
return false;
|
|
97
|
+
const innerHooks = existing.hooks;
|
|
98
|
+
if (!Array.isArray(innerHooks))
|
|
99
|
+
return false;
|
|
100
|
+
return innerHooks.some((h) => typeof h === "object" &&
|
|
101
|
+
h !== null &&
|
|
102
|
+
isBridgeHook(h));
|
|
103
|
+
});
|
|
104
|
+
if (!alreadyInstalled) {
|
|
105
|
+
eventHooks.push(entry);
|
|
106
|
+
added++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (added === 0) {
|
|
110
|
+
console.log(pc.yellow("Bridge hooks already installed. No changes made."));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (opts.dryRun) {
|
|
114
|
+
console.log(pc.cyan("Dry run — would write:"));
|
|
115
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const backupPath = backupSettings(settingsPath);
|
|
119
|
+
console.log(pc.dim(`Backup: ${backupPath}`));
|
|
120
|
+
writeSettingsRaw(settingsPath, settings);
|
|
121
|
+
console.log(pc.green(`Installed ${added} hook(s) into ${settingsPath}`));
|
|
122
|
+
console.log(pc.dim("Hooks: PostToolUse(Write), PostToolUse(Edit), Stop"));
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.error(pc.red(`Install failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BrvBridge } from "@byterover/brv-bridge";
|
|
2
|
+
import { UserPromptSubmitHookInputSchema } from "../schemas/cc-hook-input.js";
|
|
3
|
+
import { readStdinJson } from "../stdin.js";
|
|
4
|
+
export function registerRecallCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("recall")
|
|
7
|
+
.description("Query ByteRover for context relevant to the user prompt (called by UserPromptSubmit hook)")
|
|
8
|
+
.action(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const input = await readStdinJson(UserPromptSubmitHookInputSchema);
|
|
11
|
+
const { prompt, cwd } = input;
|
|
12
|
+
// Skip trivially short prompts
|
|
13
|
+
if (prompt.trim().length < 5) {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
// Query ByteRover with the actual user prompt
|
|
17
|
+
const bridge = new BrvBridge({ cwd, recallTimeoutMs: 6_000 });
|
|
18
|
+
const { content } = await bridge.recall(prompt);
|
|
19
|
+
if (!content) {
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
// Return additionalContext wrapped in hookSpecificOutput for Claude Code
|
|
23
|
+
const output = {
|
|
24
|
+
hookSpecificOutput: {
|
|
25
|
+
hookEventName: "UserPromptSubmit",
|
|
26
|
+
additionalContext: `<byterover-context>\n` +
|
|
27
|
+
`The following knowledge is from ByteRover context engine:\n\n` +
|
|
28
|
+
`${content}\n` +
|
|
29
|
+
`</byterover-context>`,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
console.log(JSON.stringify(output));
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// All errors → silent exit 0. Never block the prompt.
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=recall.js.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getCcMemoryDir } from "../memory-path.js";
|
|
4
|
+
import { StopHookInputSchema } from "../schemas/cc-hook-input.js";
|
|
5
|
+
import { readStdinJson } from "../stdin.js";
|
|
6
|
+
const BRV_CONTEXT_FILE = "_brv_context.md";
|
|
7
|
+
const CONTEXT_TREE_INDEX = ".brv/context-tree/_index.md";
|
|
8
|
+
const MEMORY_INDEX = "MEMORY.md";
|
|
9
|
+
const POINTER_LINE = "- [ByteRover context](_brv_context.md) \u2014 cross-references from project knowledge base";
|
|
10
|
+
function buildContextTreeGuide(cwd) {
|
|
11
|
+
const treePath = join(cwd, ".brv/context-tree");
|
|
12
|
+
return [
|
|
13
|
+
"<Note>",
|
|
14
|
+
`The full ByteRover context tree is located at \`${treePath}/\`.`,
|
|
15
|
+
"All paths referenced below are relative to this directory.",
|
|
16
|
+
"When you need deeper context on any topic, read the relevant files there.",
|
|
17
|
+
"Each domain contains topics with detailed narratives, facts, and code references that go beyond this summary.",
|
|
18
|
+
"</Note>",
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Strip YAML frontmatter (--- ... ---) from the beginning of a markdown string.
|
|
23
|
+
* Returns the body content after the closing --- delimiter.
|
|
24
|
+
*/
|
|
25
|
+
function stripFrontmatter(content) {
|
|
26
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n?/);
|
|
27
|
+
if (match) {
|
|
28
|
+
return content.slice(match[0].length);
|
|
29
|
+
}
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
export function registerSyncCommand(program) {
|
|
33
|
+
program
|
|
34
|
+
.command("sync")
|
|
35
|
+
.description("Regenerate _brv_context.md with cross-references from ByteRover context tree (called by Stop hook)")
|
|
36
|
+
.option("--json", "Output result as JSON")
|
|
37
|
+
.option("--memory-dir <path>", "Override path to Claude Code memory directory")
|
|
38
|
+
.action(async (opts) => {
|
|
39
|
+
try {
|
|
40
|
+
const input = await readStdinJson(StopHookInputSchema);
|
|
41
|
+
const cwd = input.cwd;
|
|
42
|
+
const memoryDir = opts.memoryDir ?? getCcMemoryDir(cwd);
|
|
43
|
+
// Read the context tree root _index.md directly from disk
|
|
44
|
+
const indexPath = join(cwd, CONTEXT_TREE_INDEX);
|
|
45
|
+
let indexBody;
|
|
46
|
+
if (existsSync(indexPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = readFileSync(indexPath, "utf-8");
|
|
49
|
+
const body = stripFrontmatter(raw).trim();
|
|
50
|
+
if (body) {
|
|
51
|
+
indexBody = body;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Read failed — fall through to stub
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Ensure memory dir exists
|
|
59
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
const contextPath = join(memoryDir, BRV_CONTEXT_FILE);
|
|
62
|
+
if (indexBody) {
|
|
63
|
+
// Success: write guide first, then context tree index body
|
|
64
|
+
const content = [
|
|
65
|
+
"---",
|
|
66
|
+
"name: byterover context",
|
|
67
|
+
"description: cross-references from ByteRover knowledge base",
|
|
68
|
+
"type: reference",
|
|
69
|
+
"---",
|
|
70
|
+
`Last synced: ${now}`,
|
|
71
|
+
"",
|
|
72
|
+
buildContextTreeGuide(cwd),
|
|
73
|
+
"",
|
|
74
|
+
indexBody,
|
|
75
|
+
"",
|
|
76
|
+
].join("\n");
|
|
77
|
+
writeFileSync(contextPath, content, "utf-8");
|
|
78
|
+
}
|
|
79
|
+
else if (!existsSync(contextPath)) {
|
|
80
|
+
// No context tree and no existing file — write stub for first run
|
|
81
|
+
const content = [
|
|
82
|
+
"---",
|
|
83
|
+
"name: byterover context",
|
|
84
|
+
"description: cross-references from ByteRover knowledge base (sync pending)",
|
|
85
|
+
"type: reference",
|
|
86
|
+
"---",
|
|
87
|
+
`Last sync attempt: ${now}`,
|
|
88
|
+
"Status: no context tree index available \u2014 .brv/context-tree/_index.md not found or empty.",
|
|
89
|
+
"",
|
|
90
|
+
].join("\n");
|
|
91
|
+
writeFileSync(contextPath, content, "utf-8");
|
|
92
|
+
}
|
|
93
|
+
// Otherwise: _index.md unavailable but _brv_context.md exists — keep existing content
|
|
94
|
+
// Add pointer to MEMORY.md if not already present (idempotent)
|
|
95
|
+
const memoryPath = join(memoryDir, MEMORY_INDEX);
|
|
96
|
+
let memoryContent = "";
|
|
97
|
+
if (existsSync(memoryPath)) {
|
|
98
|
+
memoryContent = readFileSync(memoryPath, "utf-8");
|
|
99
|
+
}
|
|
100
|
+
if (!memoryContent.includes(BRV_CONTEXT_FILE)) {
|
|
101
|
+
const separator = memoryContent.endsWith("\n") || !memoryContent
|
|
102
|
+
? ""
|
|
103
|
+
: "\n";
|
|
104
|
+
writeFileSync(memoryPath, memoryContent + separator + POINTER_LINE + "\n", "utf-8");
|
|
105
|
+
}
|
|
106
|
+
if (opts.json) {
|
|
107
|
+
console.log(JSON.stringify({
|
|
108
|
+
synced: true,
|
|
109
|
+
hasContent: !!indexBody,
|
|
110
|
+
contextPath,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
// Best-effort — never block Claude
|
|
117
|
+
process.stderr.write(`brv-claude-plugin sync error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { isBridgeHook } from "../bridge-command.js";
|
|
4
|
+
import { getClaudeConfigHome } from "../memory-path.js";
|
|
5
|
+
import { backupSettings, readSettingsRaw, writeSettingsRaw, } from "../schemas/cc-settings.js";
|
|
6
|
+
export function registerUninstallCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("uninstall")
|
|
9
|
+
.description("Remove Claude Code hooks installed by the bridge")
|
|
10
|
+
.option("--settings-path <path>", "Override path to Claude Code settings.json")
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
try {
|
|
13
|
+
const settingsPath = opts.settingsPath ??
|
|
14
|
+
join(getClaudeConfigHome(), "settings.json");
|
|
15
|
+
const settings = readSettingsRaw(settingsPath);
|
|
16
|
+
const hooks = settings.hooks;
|
|
17
|
+
if (!hooks || typeof hooks !== "object") {
|
|
18
|
+
console.log(pc.yellow("No hooks found in settings. Nothing to remove."));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let removed = 0;
|
|
22
|
+
for (const event of Object.keys(hooks)) {
|
|
23
|
+
const eventHooks = hooks[event];
|
|
24
|
+
if (!Array.isArray(eventHooks))
|
|
25
|
+
continue;
|
|
26
|
+
// Per-hook removal: within each matcher entry, remove only bridge hooks
|
|
27
|
+
for (let i = eventHooks.length - 1; i >= 0; i--) {
|
|
28
|
+
const matcherEntry = eventHooks[i];
|
|
29
|
+
const innerHooks = matcherEntry.hooks;
|
|
30
|
+
if (!Array.isArray(innerHooks))
|
|
31
|
+
continue;
|
|
32
|
+
const filtered = innerHooks.filter((h) => {
|
|
33
|
+
if (typeof h === "object" &&
|
|
34
|
+
h !== null &&
|
|
35
|
+
isBridgeHook(h)) {
|
|
36
|
+
removed++;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
if (filtered.length === 0) {
|
|
42
|
+
// All hooks in this matcher entry were bridge hooks — remove the entry
|
|
43
|
+
eventHooks.splice(i, 1);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
matcherEntry.hooks = filtered;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Clean up empty event arrays
|
|
50
|
+
if (eventHooks.length === 0) {
|
|
51
|
+
delete hooks[event];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Clean up empty hooks object
|
|
55
|
+
if (Object.keys(hooks).length === 0) {
|
|
56
|
+
delete settings.hooks;
|
|
57
|
+
}
|
|
58
|
+
if (removed === 0) {
|
|
59
|
+
console.log(pc.yellow("No bridge hooks found. Nothing to remove."));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
backupSettings(settingsPath);
|
|
63
|
+
writeSettingsRaw(settingsPath, settings);
|
|
64
|
+
console.log(pc.green(`Removed ${removed} bridge hook(s) from ${settingsPath}`));
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(pc.red(`Uninstall failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=uninstall.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function sanitizePath(name: string): string;
|
|
2
|
+
export declare function getClaudeConfigHome(): string;
|
|
3
|
+
export declare function findCanonicalGitRoot(startPath: string): string | null;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the Claude Code auto-memory directory for a given project cwd.
|
|
6
|
+
* Matches cc-ts resolution priority:
|
|
7
|
+
* 1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE env var
|
|
8
|
+
* 2. autoMemoryDirectory from settings (local → user)
|
|
9
|
+
* 3. <memoryBase>/projects/<sanitized-git-root>/memory/
|
|
10
|
+
*/
|
|
11
|
+
export declare function getCcMemoryDir(cwd: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Check if a file path is inside a cc-ts memory directory.
|
|
14
|
+
* Takes pre-resolved memoryDir for efficiency and clarity.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isCcMemoryPath(filePath: string, memoryDir: string): boolean;
|