@byterover/claude-plugin 1.0.0 → 1.1.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/README.md +129 -30
- package/dist/build-recall-output.d.ts +39 -0
- package/dist/build-recall-output.js +59 -0
- package/dist/build-visible-summary.d.ts +17 -0
- package/dist/build-visible-summary.js +40 -0
- package/dist/cli.js +26 -1
- package/dist/commands/doctor.js +39 -2
- package/dist/commands/install-statusline.d.ts +19 -0
- package/dist/commands/install-statusline.js +124 -0
- package/dist/commands/install.js +2 -3
- package/dist/commands/recall.js +8 -13
- package/dist/commands/status.d.ts +11 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/uninstall-statusline.d.ts +8 -0
- package/dist/commands/uninstall-statusline.js +54 -0
- package/dist/commands/uninstall.js +42 -36
- package/dist/format-status-line.d.ts +2 -0
- package/dist/format-status-line.js +21 -0
- package/dist/parse-sources.d.ts +16 -0
- package/dist/parse-sources.js +46 -0
- package/dist/project-data-dir.d.ts +7 -0
- package/dist/project-data-dir.js +82 -0
- package/dist/resolve-context-tree-age.d.ts +14 -0
- package/dist/resolve-context-tree-age.js +82 -0
- package/dist/state-detector.d.ts +31 -0
- package/dist/state-detector.js +123 -0
- package/package.json +3 -2
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { select } from "@inquirer/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { buildHookCommand, isBridgeHook, resolveBridgeExecutable, } from "../bridge-command.js";
|
|
5
|
+
import { getClaudeConfigHome } from "../memory-path.js";
|
|
6
|
+
import { backupSettings, readSettingsRaw, writeSettingsRaw, } from "../schemas/cc-settings.js";
|
|
7
|
+
/**
|
|
8
|
+
* Inspect a settings.json object and classify the existing `statusLine` entry:
|
|
9
|
+
* - "absent": no `statusLine` field
|
|
10
|
+
* - "ours": `statusLine.command` carries our bridge marker
|
|
11
|
+
* - "foreign": some other statusLine is configured (or malformed)
|
|
12
|
+
*/
|
|
13
|
+
export function diagnoseStatuslineConflict(settings) {
|
|
14
|
+
const sl = settings.statusLine;
|
|
15
|
+
if (sl === undefined)
|
|
16
|
+
return "absent";
|
|
17
|
+
if (typeof sl === "object" && sl !== null) {
|
|
18
|
+
const obj = sl;
|
|
19
|
+
if (typeof obj.command === "string" && isBridgeHook({ command: obj.command })) {
|
|
20
|
+
return "ours";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return "foreign";
|
|
24
|
+
}
|
|
25
|
+
/** Re-run cadence (seconds) on top of Claude Code's event-driven triggers. */
|
|
26
|
+
const REFRESH_INTERVAL_SECONDS = 5;
|
|
27
|
+
/**
|
|
28
|
+
* Set our `statusLine` entry on a settings object. Padding is intentionally
|
|
29
|
+
* omitted so Claude Code's default applies. `refreshInterval` is set so the
|
|
30
|
+
* line reflects daemon-side state changes (curate, dream) that happen while
|
|
31
|
+
* the assistant is mid-tool-call — Claude Code's event triggers don't fire
|
|
32
|
+
* during those windows. Mutates and returns `settings`.
|
|
33
|
+
*/
|
|
34
|
+
export function setStatuslineEntry(settings, command) {
|
|
35
|
+
settings.statusLine = {
|
|
36
|
+
type: "command",
|
|
37
|
+
command,
|
|
38
|
+
refreshInterval: REFRESH_INTERVAL_SECONDS,
|
|
39
|
+
};
|
|
40
|
+
return settings;
|
|
41
|
+
}
|
|
42
|
+
export function registerInstallStatuslineCommand(program) {
|
|
43
|
+
program
|
|
44
|
+
.command("install-statusline")
|
|
45
|
+
.description("Install the byterover status line into Claude Code (opt-in)")
|
|
46
|
+
.option("--dry-run", "Show what would be written without modifying files")
|
|
47
|
+
.option("--force", "Overwrite an existing foreign statusLine without prompting")
|
|
48
|
+
.option("--settings-path <path>", "Override path to Claude Code settings.json")
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
try {
|
|
51
|
+
const exe = resolveBridgeExecutable();
|
|
52
|
+
console.log(pc.dim(`Resolved executable: ${exe}`));
|
|
53
|
+
const settingsPath = opts.settingsPath ?? join(getClaudeConfigHome(), "settings.json");
|
|
54
|
+
const settings = readSettingsRaw(settingsPath);
|
|
55
|
+
const state = diagnoseStatuslineConflict(settings);
|
|
56
|
+
const command = buildHookCommand("status");
|
|
57
|
+
if (state === "ours") {
|
|
58
|
+
// Compare current entry to what we'd write. If identical, no-op.
|
|
59
|
+
// If our shape has drifted (e.g. plugin upgrade added refreshInterval),
|
|
60
|
+
// upgrade in place — backup + rewrite.
|
|
61
|
+
const expected = { type: "command", command, refreshInterval: REFRESH_INTERVAL_SECONDS };
|
|
62
|
+
if (JSON.stringify(settings.statusLine) === JSON.stringify(expected)) {
|
|
63
|
+
console.log(pc.yellow("Status line already installed. No changes made."));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Falls through to write path below — the install message will read "upgraded".
|
|
67
|
+
}
|
|
68
|
+
if (state === "foreign") {
|
|
69
|
+
const decision = await resolveForeignConflict(opts);
|
|
70
|
+
if (decision === "abort") {
|
|
71
|
+
console.log(pc.yellow("Aborted. Existing statusLine left in place."));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (decision === "keep") {
|
|
75
|
+
console.log(pc.yellow("Keeping existing statusLine. No changes made."));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// decision === "replace" — fall through
|
|
79
|
+
}
|
|
80
|
+
setStatuslineEntry(settings, command);
|
|
81
|
+
if (opts.dryRun) {
|
|
82
|
+
console.log(pc.cyan("Dry run — would write:"));
|
|
83
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const backupPath = backupSettings(settingsPath);
|
|
87
|
+
console.log(pc.dim(`Backup: ${backupPath}`));
|
|
88
|
+
writeSettingsRaw(settingsPath, settings);
|
|
89
|
+
console.log(pc.green(`Installed status line into ${settingsPath}`));
|
|
90
|
+
console.log(pc.dim(`Command: ${command}`));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (isPromptCancelled(err)) {
|
|
94
|
+
console.log(pc.yellow("\nAborted."));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
console.error(pc.red(`Install failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function resolveForeignConflict(opts) {
|
|
103
|
+
if (opts.force)
|
|
104
|
+
return "replace";
|
|
105
|
+
if (!process.stdin.isTTY) {
|
|
106
|
+
console.error(pc.red("An existing statusLine is configured. Re-run with --force to overwrite, " +
|
|
107
|
+
"or remove it first."));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
return select({
|
|
111
|
+
message: "An existing statusLine is configured. What should I do?",
|
|
112
|
+
choices: [
|
|
113
|
+
{ name: "Abort — leave it alone (default)", value: "abort" },
|
|
114
|
+
{ name: "Keep existing — install nothing", value: "keep" },
|
|
115
|
+
{ name: "Replace with byterover's status line", value: "replace" },
|
|
116
|
+
],
|
|
117
|
+
default: "abort",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export function isPromptCancelled(err) {
|
|
121
|
+
return (err instanceof Error &&
|
|
122
|
+
(err.name === "ExitPromptError" || err.message.includes("force closed")));
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=install-statusline.js.map
|
package/dist/commands/install.js
CHANGED
|
@@ -72,8 +72,7 @@ export function registerInstallCommand(program) {
|
|
|
72
72
|
// Validate executable resolves before touching settings
|
|
73
73
|
const exe = resolveBridgeExecutable();
|
|
74
74
|
console.log(pc.dim(`Resolved executable: ${exe}`));
|
|
75
|
-
const settingsPath = opts.settingsPath ??
|
|
76
|
-
join(getClaudeConfigHome(), "settings.json");
|
|
75
|
+
const settingsPath = opts.settingsPath ?? join(getClaudeConfigHome(), "settings.json");
|
|
77
76
|
const settings = readSettingsRaw(settingsPath);
|
|
78
77
|
// Ensure hooks object exists
|
|
79
78
|
if (!settings.hooks || typeof settings.hooks !== "object") {
|
|
@@ -119,7 +118,7 @@ export function registerInstallCommand(program) {
|
|
|
119
118
|
console.log(pc.dim(`Backup: ${backupPath}`));
|
|
120
119
|
writeSettingsRaw(settingsPath, settings);
|
|
121
120
|
console.log(pc.green(`Installed ${added} hook(s) into ${settingsPath}`));
|
|
122
|
-
console.log(pc.dim("Hooks: PostToolUse(Write), PostToolUse(Edit), Stop"));
|
|
121
|
+
console.log(pc.dim("Hooks: PostToolUse(Write), PostToolUse(Edit), Stop, UserPromptSubmit"));
|
|
123
122
|
}
|
|
124
123
|
catch (err) {
|
|
125
124
|
console.error(pc.red(`Install failed: ${err instanceof Error ? err.message : String(err)}`));
|
package/dist/commands/recall.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BrvBridge } from "@byterover/brv-bridge";
|
|
2
|
+
import { buildRecallOutput } from "../build-recall-output.js";
|
|
2
3
|
import { UserPromptSubmitHookInputSchema } from "../schemas/cc-hook-input.js";
|
|
3
4
|
import { readStdinJson } from "../stdin.js";
|
|
4
5
|
export function registerRecallCommand(program) {
|
|
@@ -13,22 +14,16 @@ export function registerRecallCommand(program) {
|
|
|
13
14
|
if (prompt.trim().length < 5) {
|
|
14
15
|
process.exit(0);
|
|
15
16
|
}
|
|
16
|
-
// Query ByteRover with the actual user prompt
|
|
17
|
+
// Query ByteRover with the actual user prompt. Newer brv CLIs return
|
|
18
|
+
// matchedDocs/tier/timing alongside content; older CLIs return content only, and
|
|
19
|
+
// buildRecallOutput falls back to parsing the **Sources** block in that case.
|
|
17
20
|
const bridge = new BrvBridge({ cwd, recallTimeoutMs: 6_000 });
|
|
18
|
-
const { content } = await bridge.recall(prompt);
|
|
19
|
-
|
|
21
|
+
const { content, matchedDocs } = await bridge.recall(prompt);
|
|
22
|
+
const output = buildRecallOutput({ content, matchedDocs, cwd });
|
|
23
|
+
if (!output) {
|
|
24
|
+
// No content retrieved — exit silently without injecting anything.
|
|
20
25
|
process.exit(0);
|
|
21
26
|
}
|
|
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
27
|
console.log(JSON.stringify(output));
|
|
33
28
|
process.exit(0);
|
|
34
29
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Given a parsed Claude Code status payload (or any value), return the line to
|
|
4
|
+
* print on stdout, or `""` when no `.brv/` project is reachable from the
|
|
5
|
+
* resolved cwd. Empty stdout tells Claude Code to hide the status line.
|
|
6
|
+
*
|
|
7
|
+
* `input` is `unknown` so empty/malformed stdin is graceful — when the value
|
|
8
|
+
* is not a usable object, we fall back to `process.cwd()`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function produceStatusLine(input: unknown): string;
|
|
11
|
+
export declare function registerStatusCommand(program: Command): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { formatStatusLine } from "../format-status-line.js";
|
|
2
|
+
import { detectState, findBrvDir } from "../state-detector.js";
|
|
3
|
+
/**
|
|
4
|
+
* Given a parsed Claude Code status payload (or any value), return the line to
|
|
5
|
+
* print on stdout, or `""` when no `.brv/` project is reachable from the
|
|
6
|
+
* resolved cwd. Empty stdout tells Claude Code to hide the status line.
|
|
7
|
+
*
|
|
8
|
+
* `input` is `unknown` so empty/malformed stdin is graceful — when the value
|
|
9
|
+
* is not a usable object, we fall back to `process.cwd()`.
|
|
10
|
+
*/
|
|
11
|
+
export function produceStatusLine(input) {
|
|
12
|
+
const cwd = resolveCwd(input);
|
|
13
|
+
const brvDir = findBrvDir(cwd);
|
|
14
|
+
if (brvDir === undefined)
|
|
15
|
+
return "";
|
|
16
|
+
return formatStatusLine(detectState(brvDir, cwd));
|
|
17
|
+
}
|
|
18
|
+
function resolveCwd(input) {
|
|
19
|
+
if (typeof input === "object" && input !== null) {
|
|
20
|
+
const obj = input;
|
|
21
|
+
const workspace = obj.workspace;
|
|
22
|
+
if (typeof workspace === "object" && workspace !== null) {
|
|
23
|
+
const wsDir = workspace.current_dir;
|
|
24
|
+
if (typeof wsDir === "string" && wsDir.length > 0)
|
|
25
|
+
return wsDir;
|
|
26
|
+
}
|
|
27
|
+
if (typeof obj.cwd === "string" && obj.cwd.length > 0)
|
|
28
|
+
return obj.cwd;
|
|
29
|
+
}
|
|
30
|
+
return process.cwd();
|
|
31
|
+
}
|
|
32
|
+
export function registerStatusCommand(program) {
|
|
33
|
+
program
|
|
34
|
+
.command("status")
|
|
35
|
+
.description("Print the byterover status line for Claude Code (reads CC status payload from stdin)")
|
|
36
|
+
.action(async () => {
|
|
37
|
+
const raw = await readAllStdin();
|
|
38
|
+
let input;
|
|
39
|
+
try {
|
|
40
|
+
input = raw ? JSON.parse(raw) : undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
input = undefined;
|
|
44
|
+
}
|
|
45
|
+
const line = produceStatusLine(input);
|
|
46
|
+
if (line.length > 0) {
|
|
47
|
+
process.stdout.write(line + "\n");
|
|
48
|
+
}
|
|
49
|
+
process.exit(0);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function readAllStdin() {
|
|
53
|
+
if (process.stdin.isTTY)
|
|
54
|
+
return "";
|
|
55
|
+
const chunks = [];
|
|
56
|
+
for await (const chunk of process.stdin) {
|
|
57
|
+
chunks.push(chunk);
|
|
58
|
+
}
|
|
59
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Remove the `statusLine` entry from a settings object only when it carries
|
|
4
|
+
* our bridge marker. Returns true when removed, false otherwise (foreign
|
|
5
|
+
* statusLines are left alone). Mutates `settings`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function removeOurStatusline(settings: Record<string, unknown>): boolean;
|
|
8
|
+
export declare function registerUninstallStatuslineCommand(program: Command): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Remove the `statusLine` entry from a settings object only when it carries
|
|
8
|
+
* our bridge marker. Returns true when removed, false otherwise (foreign
|
|
9
|
+
* statusLines are left alone). Mutates `settings`.
|
|
10
|
+
*/
|
|
11
|
+
export function removeOurStatusline(settings) {
|
|
12
|
+
const sl = settings.statusLine;
|
|
13
|
+
if (sl === undefined)
|
|
14
|
+
return false;
|
|
15
|
+
if (typeof sl !== "object" || sl === null)
|
|
16
|
+
return false;
|
|
17
|
+
const obj = sl;
|
|
18
|
+
if (typeof obj.command !== "string")
|
|
19
|
+
return false;
|
|
20
|
+
if (!isBridgeHook({ command: obj.command }))
|
|
21
|
+
return false;
|
|
22
|
+
delete settings.statusLine;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
export function registerUninstallStatuslineCommand(program) {
|
|
26
|
+
program
|
|
27
|
+
.command("uninstall-statusline")
|
|
28
|
+
.description("Remove the byterover status line from Claude Code settings (leaves foreign statusLines alone)")
|
|
29
|
+
.option("--settings-path <path>", "Override path to Claude Code settings.json")
|
|
30
|
+
.action(async (opts) => {
|
|
31
|
+
try {
|
|
32
|
+
const settingsPath = opts.settingsPath ?? join(getClaudeConfigHome(), "settings.json");
|
|
33
|
+
const settings = readSettingsRaw(settingsPath);
|
|
34
|
+
const removed = removeOurStatusline(settings);
|
|
35
|
+
if (!removed) {
|
|
36
|
+
if (settings.statusLine !== undefined) {
|
|
37
|
+
console.log(pc.yellow("Existing statusLine is not ours. Leaving it untouched."));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.log(pc.yellow("No status line installed. Nothing to remove."));
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
backupSettings(settingsPath);
|
|
45
|
+
writeSettingsRaw(settingsPath, settings);
|
|
46
|
+
console.log(pc.green(`Removed status line from ${settingsPath}`));
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error(pc.red(`Uninstall failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=uninstall-statusline.js.map
|
|
@@ -3,6 +3,7 @@ import pc from "picocolors";
|
|
|
3
3
|
import { isBridgeHook } from "../bridge-command.js";
|
|
4
4
|
import { getClaudeConfigHome } from "../memory-path.js";
|
|
5
5
|
import { backupSettings, readSettingsRaw, writeSettingsRaw, } from "../schemas/cc-settings.js";
|
|
6
|
+
import { removeOurStatusline } from "./uninstall-statusline.js";
|
|
6
7
|
export function registerUninstallCommand(program) {
|
|
7
8
|
program
|
|
8
9
|
.command("uninstall")
|
|
@@ -14,54 +15,59 @@ export function registerUninstallCommand(program) {
|
|
|
14
15
|
join(getClaudeConfigHome(), "settings.json");
|
|
15
16
|
const settings = readSettingsRaw(settingsPath);
|
|
16
17
|
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
18
|
let removed = 0;
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
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))
|
|
19
|
+
if (hooks && typeof hooks === "object") {
|
|
20
|
+
for (const event of Object.keys(hooks)) {
|
|
21
|
+
const eventHooks = hooks[event];
|
|
22
|
+
if (!Array.isArray(eventHooks))
|
|
31
23
|
continue;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
// Per-hook removal: within each matcher entry, remove only bridge hooks
|
|
25
|
+
for (let i = eventHooks.length - 1; i >= 0; i--) {
|
|
26
|
+
const matcherEntry = eventHooks[i];
|
|
27
|
+
const innerHooks = matcherEntry.hooks;
|
|
28
|
+
if (!Array.isArray(innerHooks))
|
|
29
|
+
continue;
|
|
30
|
+
const filtered = innerHooks.filter((h) => {
|
|
31
|
+
if (typeof h === "object" &&
|
|
32
|
+
h !== null &&
|
|
33
|
+
isBridgeHook(h)) {
|
|
34
|
+
removed++;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
if (filtered.length === 0) {
|
|
40
|
+
// All hooks in this matcher entry were bridge hooks — remove the entry
|
|
41
|
+
eventHooks.splice(i, 1);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
matcherEntry.hooks = filtered;
|
|
38
45
|
}
|
|
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
46
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
// Clean up empty event arrays
|
|
48
|
+
if (eventHooks.length === 0) {
|
|
49
|
+
delete hooks[event];
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
|
-
// Clean up empty
|
|
50
|
-
if (
|
|
51
|
-
delete hooks
|
|
52
|
+
// Clean up empty hooks object
|
|
53
|
+
if (Object.keys(hooks).length === 0) {
|
|
54
|
+
delete settings.hooks;
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (removed === 0) {
|
|
59
|
-
console.log(pc.yellow("No bridge hooks found. Nothing to remove."));
|
|
57
|
+
// Also remove our statusLine entry (foreign statusLines are left alone).
|
|
58
|
+
const statuslineRemoved = removeOurStatusline(settings);
|
|
59
|
+
if (removed === 0 && !statuslineRemoved) {
|
|
60
|
+
console.log(pc.yellow("No bridge hooks or status line found. Nothing to remove."));
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
63
|
backupSettings(settingsPath);
|
|
63
64
|
writeSettingsRaw(settingsPath, settings);
|
|
64
|
-
|
|
65
|
+
const summary = [];
|
|
66
|
+
if (removed > 0)
|
|
67
|
+
summary.push(`${removed} bridge hook(s)`);
|
|
68
|
+
if (statuslineRemoved)
|
|
69
|
+
summary.push("status line");
|
|
70
|
+
console.log(pc.green(`Removed ${summary.join(" + ")} from ${settingsPath}`));
|
|
65
71
|
}
|
|
66
72
|
catch (err) {
|
|
67
73
|
console.error(pc.red(`Uninstall failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ANSI escape sequences scoped to the state suffix only — the
|
|
2
|
+
// `🧠 ByteRover · ` brand prefix stays neutral. Raw escape codes (no
|
|
3
|
+
// picocolors) because picocolors disables colors on non-TTY stdout, but
|
|
4
|
+
// Claude Code reads our stdout from a piped context and renders the ANSI
|
|
5
|
+
// in its own TTY panel.
|
|
6
|
+
const RESET = "\x1b[0m";
|
|
7
|
+
const DIM = "\x1b[2m";
|
|
8
|
+
const YELLOW = "\x1b[33m";
|
|
9
|
+
const CYAN = "\x1b[36m";
|
|
10
|
+
const BRAND_PREFIX = "🧠 ByteRover · ";
|
|
11
|
+
export function formatStatusLine(state) {
|
|
12
|
+
switch (state) {
|
|
13
|
+
case "idle":
|
|
14
|
+
return BRAND_PREFIX + DIM + "idle" + RESET;
|
|
15
|
+
case "curating":
|
|
16
|
+
return BRAND_PREFIX + YELLOW + "📝 curating" + RESET;
|
|
17
|
+
case "dreaming":
|
|
18
|
+
return BRAND_PREFIX + CYAN + "💭 dreaming" + RESET;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=format-status-line.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-only fallback for extracting source paths from a synthesized recall response.
|
|
3
|
+
*
|
|
4
|
+
* Used when the brv CLI does not surface `matchedDocs` in the
|
|
5
|
+
* `brv query --format json` envelope. All five resolution tiers in the CLI emit a
|
|
6
|
+
* `**Sources**:` block in the synthesized content (the format is locked in by the
|
|
7
|
+
* synthesis prompt's `responseFormat` rules and grounding rules), so we can recover paths
|
|
8
|
+
* by parsing the content. When the CLI is current we use `matchedDocs` directly and skip
|
|
9
|
+
* this parser entirely.
|
|
10
|
+
*
|
|
11
|
+
* Returns an empty array on any of:
|
|
12
|
+
* - No `**Sources**:` block at all
|
|
13
|
+
* - `**Sources**: None` (Tier 2 not-found)
|
|
14
|
+
* - Block exists but contains no extractable list items
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseSourcesFromContent(content: string): string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-only fallback for extracting source paths from a synthesized recall response.
|
|
3
|
+
*
|
|
4
|
+
* Used when the brv CLI does not surface `matchedDocs` in the
|
|
5
|
+
* `brv query --format json` envelope. All five resolution tiers in the CLI emit a
|
|
6
|
+
* `**Sources**:` block in the synthesized content (the format is locked in by the
|
|
7
|
+
* synthesis prompt's `responseFormat` rules and grounding rules), so we can recover paths
|
|
8
|
+
* by parsing the content. When the CLI is current we use `matchedDocs` directly and skip
|
|
9
|
+
* this parser entirely.
|
|
10
|
+
*
|
|
11
|
+
* Returns an empty array on any of:
|
|
12
|
+
* - No `**Sources**:` block at all
|
|
13
|
+
* - `**Sources**: None` (Tier 2 not-found)
|
|
14
|
+
* - Block exists but contains no extractable list items
|
|
15
|
+
*/
|
|
16
|
+
const PREFIX_TO_STRIP = ".brv/context-tree/";
|
|
17
|
+
// Capture from "**Sources**:" through the next blank-line-then-heading boundary OR end of string.
|
|
18
|
+
// Backticked items are isolated by a non-greedy capture; non-list lines after Sources end the block.
|
|
19
|
+
const SOURCES_BLOCK_REGEX = /\*\*Sources\*\*:\s*\n([\s\S]*?)(?:\n\s*\n|$)/;
|
|
20
|
+
// Match list items with OR without backtick fencing — Tier 2 direct-search emits backticked paths
|
|
21
|
+
// (`- \`path\``) but LLM-synthesised output in Tier 3/4 often drops the backticks (`- path`).
|
|
22
|
+
// Backtick group is optional so both render the same captured path.
|
|
23
|
+
const LIST_ITEM_REGEX = /^\s*-\s*`?([^`\n]+?)`?\s*$/gm;
|
|
24
|
+
export function parseSourcesFromContent(content) {
|
|
25
|
+
if (!content)
|
|
26
|
+
return [];
|
|
27
|
+
const blockMatch = content.match(SOURCES_BLOCK_REGEX);
|
|
28
|
+
if (!blockMatch)
|
|
29
|
+
return [];
|
|
30
|
+
const block = blockMatch[1] ?? "";
|
|
31
|
+
// "Sources: None" inline (no list items) → empty
|
|
32
|
+
if (/^\s*None\s*$/i.test(block))
|
|
33
|
+
return [];
|
|
34
|
+
const paths = [];
|
|
35
|
+
for (const itemMatch of block.matchAll(LIST_ITEM_REGEX)) {
|
|
36
|
+
const raw = itemMatch[1] ?? "";
|
|
37
|
+
paths.push(stripPrefix(raw));
|
|
38
|
+
}
|
|
39
|
+
return paths;
|
|
40
|
+
}
|
|
41
|
+
function stripPrefix(path) {
|
|
42
|
+
return path.startsWith(PREFIX_TO_STRIP)
|
|
43
|
+
? path.slice(PREFIX_TO_STRIP.length)
|
|
44
|
+
: path;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=parse-sources.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function getGlobalDataDir(): string;
|
|
2
|
+
export declare function sanitizeProjectPath(resolvedPath: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* `<getGlobalDataDir()>/projects/<sanitizeProjectPath(realpath(cwd))>/`
|
|
5
|
+
* Throws if `cwd` does not exist (matches daemon's `realpathSync` semantics).
|
|
6
|
+
*/
|
|
7
|
+
export declare function getProjectDataDir(cwd: string): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Mirrors byterover-cli's project data dir resolution. Kept narrowly in sync
|
|
7
|
+
* with byterover-cli's `src/server/utils/path-utils.ts` and
|
|
8
|
+
* `src/server/utils/global-data-path.ts` so we can locate per-project artifacts
|
|
9
|
+
* (curate-log, query-log) the daemon writes outside the project's own `.brv/`.
|
|
10
|
+
*
|
|
11
|
+
* Daemon contract reproduced here:
|
|
12
|
+
* <getGlobalDataDir()>/projects/<sanitizeProjectPath(realpath(cwd))>/
|
|
13
|
+
*
|
|
14
|
+
* If the daemon ever changes either function, this module must be updated in
|
|
15
|
+
* lockstep — there is no runtime contract enforcing parity.
|
|
16
|
+
*/
|
|
17
|
+
const GLOBAL_DATA_DIR = "brv";
|
|
18
|
+
const GLOBAL_PROJECTS_DIR = "projects";
|
|
19
|
+
const MAX_SANITIZED_LENGTH = 200;
|
|
20
|
+
const HASH_SUFFIX_LENGTH = 12;
|
|
21
|
+
const WINDOWS_ILLEGAL_CHARS = new Map([
|
|
22
|
+
['"', "%22"],
|
|
23
|
+
["*", "%2A"],
|
|
24
|
+
[":", "%3A"],
|
|
25
|
+
["<", "%3C"],
|
|
26
|
+
[">", "%3E"],
|
|
27
|
+
["?", "%3F"],
|
|
28
|
+
["|", "%7C"],
|
|
29
|
+
]);
|
|
30
|
+
export function getGlobalDataDir() {
|
|
31
|
+
if (process.env.BRV_DATA_DIR !== undefined && process.env.BRV_DATA_DIR !== "") {
|
|
32
|
+
return process.env.BRV_DATA_DIR;
|
|
33
|
+
}
|
|
34
|
+
const p = platform();
|
|
35
|
+
if (p === "win32") {
|
|
36
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
37
|
+
if (localAppData !== undefined && localAppData !== "") {
|
|
38
|
+
return join(localAppData, GLOBAL_DATA_DIR);
|
|
39
|
+
}
|
|
40
|
+
return join(homedir(), "AppData", "Local", GLOBAL_DATA_DIR);
|
|
41
|
+
}
|
|
42
|
+
if (p === "darwin") {
|
|
43
|
+
return join(homedir(), "Library", "Application Support", GLOBAL_DATA_DIR);
|
|
44
|
+
}
|
|
45
|
+
if (p === "linux") {
|
|
46
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
47
|
+
if (xdgDataHome !== undefined && xdgDataHome !== "") {
|
|
48
|
+
return join(xdgDataHome, GLOBAL_DATA_DIR);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return join(homedir(), ".local", "share", GLOBAL_DATA_DIR);
|
|
52
|
+
}
|
|
53
|
+
export function sanitizeProjectPath(resolvedPath) {
|
|
54
|
+
const normalized = resolvedPath.replace(/^([A-Za-z]):/, "$1");
|
|
55
|
+
const components = normalized.split(/[/\\]+/).filter(Boolean);
|
|
56
|
+
const encoded = components.map((c) => {
|
|
57
|
+
let result = c.replaceAll("%", "%25").replaceAll("--", "%2D%2D");
|
|
58
|
+
for (const [char, replacement] of WINDOWS_ILLEGAL_CHARS) {
|
|
59
|
+
result = result.replaceAll(char, replacement);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
});
|
|
63
|
+
const joined = encoded.join("--");
|
|
64
|
+
if (joined.length <= MAX_SANITIZED_LENGTH) {
|
|
65
|
+
return joined;
|
|
66
|
+
}
|
|
67
|
+
const hash = createHash("sha256")
|
|
68
|
+
.update(joined)
|
|
69
|
+
.digest("hex")
|
|
70
|
+
.slice(0, HASH_SUFFIX_LENGTH);
|
|
71
|
+
const prefixLength = MAX_SANITIZED_LENGTH - HASH_SUFFIX_LENGTH - 3;
|
|
72
|
+
return joined.slice(0, prefixLength) + "---" + hash;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `<getGlobalDataDir()>/projects/<sanitizeProjectPath(realpath(cwd))>/`
|
|
76
|
+
* Throws if `cwd` does not exist (matches daemon's `realpathSync` semantics).
|
|
77
|
+
*/
|
|
78
|
+
export function getProjectDataDir(cwd) {
|
|
79
|
+
const resolved = realpathSync(cwd);
|
|
80
|
+
return join(getGlobalDataDir(), GLOBAL_PROJECTS_DIR, sanitizeProjectPath(resolved));
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=project-data-dir.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the "age" of a context-tree document for the visible recall summary.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order, falling through on any failure:
|
|
5
|
+
* 1. Frontmatter `updatedAt` — standard topic files (post-curate timestamp)
|
|
6
|
+
* 2. Frontmatter `synthesized_at` — synthesis files produced by `brv dream`
|
|
7
|
+
* 3. Frontmatter `createdAt` — older standard files that lack updatedAt
|
|
8
|
+
* 4. File `mtime` — last resort for files without timestamp frontmatter
|
|
9
|
+
* 5. `undefined` — file missing or path is a cross-project shared source
|
|
10
|
+
*
|
|
11
|
+
* Cross-project paths in the form `[alias]:relative/path.md` are not resolved here —
|
|
12
|
+
* they live in another project's `.brv/context-tree/`, which the plugin does not see.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveContextTreeAge(projectRoot: string, relativePath: string): Date | undefined;
|