@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.
@@ -0,0 +1,214 @@
1
+ import { readFileSync, realpathSync, statSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, isAbsolute, join, normalize, resolve, sep, } from "node:path";
4
+ // ---------------------------------------------------------------------------
5
+ // djb2 hash — exact match of cc-ts/utils/hash.ts
6
+ // ---------------------------------------------------------------------------
7
+ function djb2Hash(str) {
8
+ let hash = 0;
9
+ for (let i = 0; i < str.length; i++) {
10
+ hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
11
+ }
12
+ return hash;
13
+ }
14
+ function simpleHash(str) {
15
+ return Math.abs(djb2Hash(str)).toString(36);
16
+ }
17
+ // ---------------------------------------------------------------------------
18
+ // Path sanitization — exact match of cc-ts/utils/sessionStoragePortable.ts
19
+ // ---------------------------------------------------------------------------
20
+ const MAX_SANITIZED_LENGTH = 200;
21
+ export function sanitizePath(name) {
22
+ const sanitized = name.replace(/[^a-zA-Z0-9]/g, "-");
23
+ if (sanitized.length <= MAX_SANITIZED_LENGTH) {
24
+ return sanitized;
25
+ }
26
+ return `${sanitized.slice(0, MAX_SANITIZED_LENGTH)}-${simpleHash(name)}`;
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Claude config home
30
+ // ---------------------------------------------------------------------------
31
+ export function getClaudeConfigHome() {
32
+ return (process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude")).normalize("NFC");
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // Memory base directory
36
+ // ---------------------------------------------------------------------------
37
+ function getMemoryBaseDir() {
38
+ return process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR ?? getClaudeConfigHome();
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // Memory path validation — matches cc-ts/memdir/paths.ts validateMemoryPath
42
+ // ---------------------------------------------------------------------------
43
+ function validateMemoryPath(raw, expandTilde) {
44
+ if (!raw || typeof raw !== "string")
45
+ return undefined;
46
+ let candidate = raw;
47
+ // Expand ~/ if allowed (settings: yes, env var: no)
48
+ if (expandTilde &&
49
+ (candidate.startsWith("~/") || candidate.startsWith("~\\"))) {
50
+ candidate = join(homedir(), candidate.slice(2));
51
+ }
52
+ // Normalize and strip trailing separators
53
+ const normalized = normalize(candidate).replace(/[/\\]+$/, "");
54
+ // Reject non-absolute, too short, or dangerous paths
55
+ if (!isAbsolute(normalized))
56
+ return undefined;
57
+ if (normalized.length < 3)
58
+ return undefined;
59
+ if (normalized.includes("\0"))
60
+ return undefined;
61
+ // Reject UNC paths on Windows
62
+ if (normalized.startsWith("\\\\"))
63
+ return undefined;
64
+ // Reject paths that resolve to home dir, its parent, or root-like dirs.
65
+ // A malicious settings file with autoMemoryDirectory: "~/.." would otherwise
66
+ // redirect bridge writes to /Users/ (or equivalent).
67
+ const home = homedir();
68
+ const homeParent = dirname(home);
69
+ if (normalized === home ||
70
+ normalized === homeParent ||
71
+ normalized === dirname(homeParent)) {
72
+ return undefined;
73
+ }
74
+ return (normalized + sep).normalize("NFC");
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Settings-based override — best local approximation of cc-ts precedence
78
+ // ---------------------------------------------------------------------------
79
+ function readAutoMemDirFromSettings(filePath) {
80
+ try {
81
+ const content = readFileSync(filePath, "utf-8");
82
+ const settings = JSON.parse(content);
83
+ if (typeof settings.autoMemoryDirectory === "string" &&
84
+ settings.autoMemoryDirectory) {
85
+ return settings.autoMemoryDirectory;
86
+ }
87
+ }
88
+ catch {
89
+ // file doesn't exist or isn't valid JSON
90
+ }
91
+ return undefined;
92
+ }
93
+ function getAutoMemPathSetting(cwd) {
94
+ // Best local approximation of cc-ts precedence:
95
+ // policySettings → flagSettings → localSettings → userSettings
96
+ // We cannot access policySettings or flagSettings from a hook process.
97
+ // Env-var overrides (priority 1 & 2) cover those cases in practice.
98
+ // localSettings: <cwd>/.claude/settings.local.json
99
+ const localDir = readAutoMemDirFromSettings(join(cwd, ".claude", "settings.local.json"));
100
+ if (localDir)
101
+ return validateMemoryPath(localDir, true);
102
+ // userSettings: ~/.claude/settings.json
103
+ const userDir = readAutoMemDirFromSettings(join(getClaudeConfigHome(), "settings.json"));
104
+ if (userDir)
105
+ return validateMemoryPath(userDir, true);
106
+ return undefined;
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ // Full override via env var
110
+ // ---------------------------------------------------------------------------
111
+ function getAutoMemPathOverride() {
112
+ const raw = process.env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE;
113
+ return validateMemoryPath(raw, false);
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Canonical git root — walk up, follow worktree chain
117
+ // Ported from cc-ts/utils/git.ts
118
+ // ---------------------------------------------------------------------------
119
+ function findGitRoot(startPath) {
120
+ let current = resolve(startPath);
121
+ const root = current.substring(0, current.indexOf(sep) + 1) || sep;
122
+ while (current !== root) {
123
+ try {
124
+ const gitPath = join(current, ".git");
125
+ const st = statSync(gitPath);
126
+ if (st.isDirectory() || st.isFile()) {
127
+ return current.normalize("NFC");
128
+ }
129
+ }
130
+ catch {
131
+ // .git doesn't exist here
132
+ }
133
+ const parent = dirname(current);
134
+ if (parent === current)
135
+ break;
136
+ current = parent;
137
+ }
138
+ // Check root
139
+ try {
140
+ const st = statSync(join(root, ".git"));
141
+ if (st.isDirectory() || st.isFile()) {
142
+ return root.normalize("NFC");
143
+ }
144
+ }
145
+ catch {
146
+ // not in git
147
+ }
148
+ return null;
149
+ }
150
+ function resolveCanonicalRoot(gitRoot) {
151
+ try {
152
+ const gitContent = readFileSync(join(gitRoot, ".git"), "utf-8").trim();
153
+ if (!gitContent.startsWith("gitdir:")) {
154
+ return gitRoot;
155
+ }
156
+ const worktreeGitDir = resolve(gitRoot, gitContent.slice("gitdir:".length).trim());
157
+ // commondir → shared .git directory. Submodules have none → falls through.
158
+ const commonDir = resolve(worktreeGitDir, readFileSync(join(worktreeGitDir, "commondir"), "utf-8").trim());
159
+ // Security: validate worktree chain structure
160
+ if (resolve(dirname(worktreeGitDir)) !== join(commonDir, "worktrees")) {
161
+ return gitRoot;
162
+ }
163
+ const backlink = realpathSync(readFileSync(join(worktreeGitDir, "gitdir"), "utf-8").trim());
164
+ if (backlink !== join(realpathSync(gitRoot), ".git")) {
165
+ return gitRoot;
166
+ }
167
+ // Bare-repo worktrees: common dir isn't inside a working directory
168
+ if (basename(commonDir) !== ".git") {
169
+ return commonDir.normalize("NFC");
170
+ }
171
+ return dirname(commonDir).normalize("NFC");
172
+ }
173
+ catch {
174
+ return gitRoot;
175
+ }
176
+ }
177
+ export function findCanonicalGitRoot(startPath) {
178
+ const root = findGitRoot(startPath);
179
+ if (!root)
180
+ return null;
181
+ return resolveCanonicalRoot(root);
182
+ }
183
+ // ---------------------------------------------------------------------------
184
+ // Public API
185
+ // ---------------------------------------------------------------------------
186
+ /**
187
+ * Resolve the Claude Code auto-memory directory for a given project cwd.
188
+ * Matches cc-ts resolution priority:
189
+ * 1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE env var
190
+ * 2. autoMemoryDirectory from settings (local → user)
191
+ * 3. <memoryBase>/projects/<sanitized-git-root>/memory/
192
+ */
193
+ export function getCcMemoryDir(cwd) {
194
+ // Priority 1: full path override
195
+ const override = getAutoMemPathOverride();
196
+ if (override)
197
+ return override;
198
+ // Priority 2: settings override
199
+ const setting = getAutoMemPathSetting(cwd);
200
+ if (setting)
201
+ return setting;
202
+ // Priority 3: computed from canonical git root
203
+ const gitRoot = findCanonicalGitRoot(cwd) ?? cwd;
204
+ const projectsDir = join(getMemoryBaseDir(), "projects");
205
+ return (join(projectsDir, sanitizePath(gitRoot), "memory") + sep).normalize("NFC");
206
+ }
207
+ /**
208
+ * Check if a file path is inside a cc-ts memory directory.
209
+ * Takes pre-resolved memoryDir for efficiency and clarity.
210
+ */
211
+ export function isCcMemoryPath(filePath, memoryDir) {
212
+ return filePath.startsWith(memoryDir);
213
+ }
214
+ //# sourceMappingURL=memory-path.js.map
@@ -0,0 +1,129 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Schema for the JSON that Claude Code pipes to hook commands via stdin.
4
+ * Matches upstream: cc-ts/entrypoints/sdk/coreSchemas.ts
5
+ */
6
+ export declare const BaseHookInputSchema: z.ZodObject<{
7
+ session_id: z.ZodString;
8
+ transcript_path: z.ZodString;
9
+ cwd: z.ZodString;
10
+ permission_mode: z.ZodOptional<z.ZodString>;
11
+ agent_id: z.ZodOptional<z.ZodString>;
12
+ agent_type: z.ZodOptional<z.ZodString>;
13
+ }, "strip", z.ZodTypeAny, {
14
+ session_id: string;
15
+ transcript_path: string;
16
+ cwd: string;
17
+ permission_mode?: string | undefined;
18
+ agent_id?: string | undefined;
19
+ agent_type?: string | undefined;
20
+ }, {
21
+ session_id: string;
22
+ transcript_path: string;
23
+ cwd: string;
24
+ permission_mode?: string | undefined;
25
+ agent_id?: string | undefined;
26
+ agent_type?: string | undefined;
27
+ }>;
28
+ export declare const PostToolUseHookInputSchema: z.ZodObject<{
29
+ session_id: z.ZodString;
30
+ transcript_path: z.ZodString;
31
+ cwd: z.ZodString;
32
+ permission_mode: z.ZodOptional<z.ZodString>;
33
+ agent_id: z.ZodOptional<z.ZodString>;
34
+ agent_type: z.ZodOptional<z.ZodString>;
35
+ } & {
36
+ hook_event_name: z.ZodLiteral<"PostToolUse">;
37
+ tool_name: z.ZodString;
38
+ tool_input: z.ZodUnknown;
39
+ tool_response: z.ZodUnknown;
40
+ tool_use_id: z.ZodString;
41
+ }, "strip", z.ZodTypeAny, {
42
+ session_id: string;
43
+ transcript_path: string;
44
+ cwd: string;
45
+ hook_event_name: "PostToolUse";
46
+ tool_name: string;
47
+ tool_use_id: string;
48
+ permission_mode?: string | undefined;
49
+ agent_id?: string | undefined;
50
+ agent_type?: string | undefined;
51
+ tool_input?: unknown;
52
+ tool_response?: unknown;
53
+ }, {
54
+ session_id: string;
55
+ transcript_path: string;
56
+ cwd: string;
57
+ hook_event_name: "PostToolUse";
58
+ tool_name: string;
59
+ tool_use_id: string;
60
+ permission_mode?: string | undefined;
61
+ agent_id?: string | undefined;
62
+ agent_type?: string | undefined;
63
+ tool_input?: unknown;
64
+ tool_response?: unknown;
65
+ }>;
66
+ export declare const StopHookInputSchema: z.ZodObject<{
67
+ session_id: z.ZodString;
68
+ transcript_path: z.ZodString;
69
+ cwd: z.ZodString;
70
+ permission_mode: z.ZodOptional<z.ZodString>;
71
+ agent_id: z.ZodOptional<z.ZodString>;
72
+ agent_type: z.ZodOptional<z.ZodString>;
73
+ } & {
74
+ hook_event_name: z.ZodLiteral<"Stop">;
75
+ stop_hook_active: z.ZodBoolean;
76
+ last_assistant_message: z.ZodOptional<z.ZodString>;
77
+ }, "strip", z.ZodTypeAny, {
78
+ session_id: string;
79
+ transcript_path: string;
80
+ cwd: string;
81
+ hook_event_name: "Stop";
82
+ stop_hook_active: boolean;
83
+ permission_mode?: string | undefined;
84
+ agent_id?: string | undefined;
85
+ agent_type?: string | undefined;
86
+ last_assistant_message?: string | undefined;
87
+ }, {
88
+ session_id: string;
89
+ transcript_path: string;
90
+ cwd: string;
91
+ hook_event_name: "Stop";
92
+ stop_hook_active: boolean;
93
+ permission_mode?: string | undefined;
94
+ agent_id?: string | undefined;
95
+ agent_type?: string | undefined;
96
+ last_assistant_message?: string | undefined;
97
+ }>;
98
+ export declare const UserPromptSubmitHookInputSchema: z.ZodObject<{
99
+ session_id: z.ZodString;
100
+ transcript_path: z.ZodString;
101
+ cwd: z.ZodString;
102
+ permission_mode: z.ZodOptional<z.ZodString>;
103
+ agent_id: z.ZodOptional<z.ZodString>;
104
+ agent_type: z.ZodOptional<z.ZodString>;
105
+ } & {
106
+ hook_event_name: z.ZodLiteral<"UserPromptSubmit">;
107
+ prompt: z.ZodString;
108
+ }, "strip", z.ZodTypeAny, {
109
+ session_id: string;
110
+ transcript_path: string;
111
+ cwd: string;
112
+ hook_event_name: "UserPromptSubmit";
113
+ prompt: string;
114
+ permission_mode?: string | undefined;
115
+ agent_id?: string | undefined;
116
+ agent_type?: string | undefined;
117
+ }, {
118
+ session_id: string;
119
+ transcript_path: string;
120
+ cwd: string;
121
+ hook_event_name: "UserPromptSubmit";
122
+ prompt: string;
123
+ permission_mode?: string | undefined;
124
+ agent_id?: string | undefined;
125
+ agent_type?: string | undefined;
126
+ }>;
127
+ export type PostToolUseHookInput = z.infer<typeof PostToolUseHookInputSchema>;
128
+ export type StopHookInput = z.infer<typeof StopHookInputSchema>;
129
+ export type UserPromptSubmitHookInput = z.infer<typeof UserPromptSubmitHookInputSchema>;
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Schema for the JSON that Claude Code pipes to hook commands via stdin.
4
+ * Matches upstream: cc-ts/entrypoints/sdk/coreSchemas.ts
5
+ */
6
+ export const BaseHookInputSchema = z.object({
7
+ session_id: z.string(),
8
+ transcript_path: z.string(),
9
+ cwd: z.string(),
10
+ permission_mode: z.string().optional(),
11
+ agent_id: z.string().optional(),
12
+ agent_type: z.string().optional(),
13
+ });
14
+ export const PostToolUseHookInputSchema = BaseHookInputSchema.extend({
15
+ hook_event_name: z.literal("PostToolUse"),
16
+ tool_name: z.string(),
17
+ tool_input: z.unknown(),
18
+ tool_response: z.unknown(),
19
+ tool_use_id: z.string(),
20
+ });
21
+ export const StopHookInputSchema = BaseHookInputSchema.extend({
22
+ hook_event_name: z.literal("Stop"),
23
+ stop_hook_active: z.boolean(),
24
+ last_assistant_message: z.string().optional(),
25
+ });
26
+ export const UserPromptSubmitHookInputSchema = BaseHookInputSchema.extend({
27
+ hook_event_name: z.literal("UserPromptSubmit"),
28
+ prompt: z.string(),
29
+ });
30
+ //# sourceMappingURL=cc-hook-input.js.map
@@ -0,0 +1,24 @@
1
+ export interface BridgeCommandHook {
2
+ type: "command";
3
+ command: string;
4
+ async: boolean;
5
+ timeout: number;
6
+ }
7
+ export interface BridgeHookEntry {
8
+ matcher?: string;
9
+ hooks: BridgeCommandHook[];
10
+ }
11
+ /**
12
+ * Read settings.json as a raw JSON object. Returns empty object if missing.
13
+ * Never validates with Zod — preserves all upstream fields verbatim.
14
+ */
15
+ export declare function readSettingsRaw(path: string): Record<string, unknown>;
16
+ /**
17
+ * Write settings.json, preserving formatting.
18
+ * Creates parent directories if needed.
19
+ */
20
+ export declare function writeSettingsRaw(path: string, data: Record<string, unknown>): void;
21
+ /**
22
+ * Backup settings.json before modification.
23
+ */
24
+ export declare function backupSettings(path: string): string;
@@ -0,0 +1,35 @@
1
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ // ---------------------------------------------------------------------------
4
+ // Raw JSON read/write — preserves all unknown fields
5
+ // ---------------------------------------------------------------------------
6
+ /**
7
+ * Read settings.json as a raw JSON object. Returns empty object if missing.
8
+ * Never validates with Zod — preserves all upstream fields verbatim.
9
+ */
10
+ export function readSettingsRaw(path) {
11
+ if (!existsSync(path)) {
12
+ return {};
13
+ }
14
+ const content = readFileSync(path, "utf-8");
15
+ return JSON.parse(content);
16
+ }
17
+ /**
18
+ * Write settings.json, preserving formatting.
19
+ * Creates parent directories if needed.
20
+ */
21
+ export function writeSettingsRaw(path, data) {
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
24
+ }
25
+ /**
26
+ * Backup settings.json before modification.
27
+ */
28
+ export function backupSettings(path) {
29
+ const backupPath = path + ".brv-backup";
30
+ if (existsSync(path)) {
31
+ copyFileSync(path, backupPath);
32
+ }
33
+ return backupPath;
34
+ }
35
+ //# sourceMappingURL=cc-settings.js.map
@@ -0,0 +1,7 @@
1
+ import type { ZodSchema } from "zod";
2
+ /**
3
+ * Read all data from stdin, parse as JSON, and validate with a Zod schema.
4
+ * Used by hook commands (ingest, sync) to read the JSON that Claude Code
5
+ * pipes to hook processes.
6
+ */
7
+ export declare function readStdinJson<T>(schema: ZodSchema<T>): Promise<T>;
package/dist/stdin.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Read all data from stdin, parse as JSON, and validate with a Zod schema.
3
+ * Used by hook commands (ingest, sync) to read the JSON that Claude Code
4
+ * pipes to hook processes.
5
+ */
6
+ export async function readStdinJson(schema) {
7
+ const chunks = [];
8
+ for await (const chunk of process.stdin) {
9
+ chunks.push(chunk);
10
+ }
11
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
12
+ if (!raw) {
13
+ throw new Error("No input received on stdin");
14
+ }
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(raw);
18
+ }
19
+ catch {
20
+ throw new Error(`Invalid JSON on stdin: ${raw.slice(0, 200)}`);
21
+ }
22
+ return schema.parse(parsed);
23
+ }
24
+ //# sourceMappingURL=stdin.js.map
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@byterover/claude-plugin",
3
+ "version": "1.0.0",
4
+ "description": "Native bridge between ByteRover context engine and Claude Code — enriches Claude's auto-memory with BM25-ranked knowledge from brv context tree",
5
+ "type": "module",
6
+ "bin": {
7
+ "brv-claude-plugin": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/cli.js",
10
+ "scripts": {
11
+ "build": "rm -rf dist && tsc",
12
+ "dev": "tsx src/cli.ts",
13
+ "test": "vitest run --dir test",
14
+ "typecheck": "tsc --noEmit",
15
+ "lint": "eslint src/",
16
+ "clean": "rm -rf dist",
17
+ "prepublishOnly": "npm run build && npm test"
18
+ },
19
+ "files": [
20
+ "dist/**/*.js",
21
+ "dist/**/*.d.ts",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "dependencies": {
26
+ "@byterover/brv-bridge": "^1.0.2",
27
+ "commander": "^13.0.0",
28
+ "js-yaml": "^4.1.0",
29
+ "picocolors": "^1.1.0",
30
+ "zod": "^3.24.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/js-yaml": "^4.0.0",
34
+ "@types/node": "^22.0.0",
35
+ "tsx": "^4.19.0",
36
+ "typescript": "^5.7.0",
37
+ "vitest": "^3.0.0"
38
+ },
39
+ "keywords": [
40
+ "byterover",
41
+ "claude-code",
42
+ "memory",
43
+ "context-engine",
44
+ "bridge"
45
+ ],
46
+ "author": "ByteRover",
47
+ "license": "Elastic-2.0",
48
+ "engines": {
49
+ "node": ">=20"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/campfirein/brv-claude-plugin.git"
54
+ },
55
+ "homepage": "https://github.com/campfirein/brv-claude-plugin#readme",
56
+ "bugs": {
57
+ "url": "https://github.com/campfirein/brv-claude-plugin/issues"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }