pi-byterover 0.2.4 → 0.2.5
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 +8 -6
- package/package.json +19 -42
- package/src/config-loader.ts +58 -0
- package/src/config.ts +76 -0
- package/src/gitignore.ts +84 -0
- package/src/index.ts +348 -0
- package/src/lru-cache.ts +24 -0
- package/src/messages.ts +94 -0
- package/src/recall.ts +19 -0
- package/src/tools.ts +221 -0
- package/dist/config-loader.d.ts +0 -17
- package/dist/config.d.ts +0 -38
- package/dist/gitignore.d.ts +0 -2
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -5145
- package/dist/lru-cache.d.ts +0 -7
- package/dist/messages.d.ts +0 -15
- package/dist/recall.d.ts +0 -1
- package/dist/tools.d.ts +0 -19
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<p>
|
|
8
8
|
<a href="https://www.npmjs.com/package/pi-byterover"><img src="https://img.shields.io/npm/v/pi-byterover?style=for-the-badge&color=5B8DEF" alt="npm version"></a>
|
|
9
9
|
<a href="LICENSE"><img src="https://img.shields.io/npm/l/pi-byterover?style=for-the-badge&color=34D399" alt="license"></a>
|
|
10
|
-
<a href="https://github.com/ian-pascoe/pi-byterover"><img src="https://img.shields.io/badge/github-pi--
|
|
10
|
+
<a href="https://github.com/ian-pascoe/pi-extensions/tree/main/packages/pi-byterover"><img src="https://img.shields.io/badge/github-pi--extensions-111827?style=for-the-badge&logo=github" alt="GitHub repository"></a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
</div>
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
It automatically recalls relevant ByteRover memory before an agent response and injects that context into the session. After useful completed turns, it persists durable facts, decisions, preferences, and technical details back to ByteRover.
|
|
18
18
|
|
|
19
|
+
Recalled memory is framed as **untrusted reference material**. The extension tells the agent not to treat recalled text as system, developer, user, or tool instructions.
|
|
20
|
+
|
|
19
21
|
## Prerequisites
|
|
20
22
|
|
|
21
23
|
- Pi installed.
|
|
24
|
+
- Node `>=22.19.0` and Pi `>=0.84.1`.
|
|
22
25
|
- ByteRover CLI installed as `brv`, or a custom executable path configured with `brvPath`.
|
|
23
26
|
- A project where ByteRover can bootstrap and use `.brv` state.
|
|
24
27
|
|
|
@@ -34,8 +37,8 @@ For local development:
|
|
|
34
37
|
|
|
35
38
|
```bash
|
|
36
39
|
pnpm install
|
|
37
|
-
pnpm
|
|
38
|
-
pi -e ./
|
|
40
|
+
pnpm --filter pi-byterover test
|
|
41
|
+
pi -e ./src/index.ts
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
## Configuration
|
|
@@ -103,7 +106,6 @@ With `autoRecall` and `autoPersist` enabled, routine memory behavior is automati
|
|
|
103
106
|
pnpm install
|
|
104
107
|
pnpm format:check
|
|
105
108
|
pnpm lint
|
|
106
|
-
pnpm test
|
|
107
|
-
pnpm typecheck
|
|
108
|
-
pnpm build
|
|
109
|
+
pnpm --filter pi-byterover test
|
|
110
|
+
pnpm --filter pi-byterover typecheck
|
|
109
111
|
```
|
package/package.json
CHANGED
|
@@ -1,37 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-byterover",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Pi extension that recalls and persists ByteRover memory.",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"byterover",
|
|
7
8
|
"memory",
|
|
8
9
|
"pi",
|
|
10
|
+
"pi-extension",
|
|
9
11
|
"pi-package"
|
|
10
12
|
],
|
|
11
|
-
"homepage": "https://github.com/ian-pascoe/pi-
|
|
13
|
+
"homepage": "https://github.com/ian-pascoe/pi-extensions#readme",
|
|
12
14
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/ian-pascoe/pi-
|
|
15
|
+
"url": "https://github.com/ian-pascoe/pi-extensions/issues"
|
|
14
16
|
},
|
|
15
17
|
"license": "MIT",
|
|
16
|
-
"author": "Ian Pascoe",
|
|
18
|
+
"author": "Ian Pascoe <ian.g.pascoe@gmail.com>",
|
|
17
19
|
"repository": {
|
|
18
20
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/ian-pascoe/pi-
|
|
21
|
+
"url": "git+https://github.com/ian-pascoe/pi-extensions.git",
|
|
22
|
+
"directory": "packages/pi-byterover"
|
|
20
23
|
},
|
|
21
24
|
"files": [
|
|
22
|
-
"
|
|
23
|
-
"README.md"
|
|
25
|
+
"src",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
24
28
|
],
|
|
25
29
|
"type": "module",
|
|
26
|
-
"main": "dist/index.js",
|
|
27
|
-
"types": "dist/index.d.ts",
|
|
28
|
-
"exports": {
|
|
29
|
-
".": {
|
|
30
|
-
"types": "./dist/index.d.ts",
|
|
31
|
-
"import": "./dist/index.js",
|
|
32
|
-
"default": "./dist/index.js"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
30
|
"publishConfig": {
|
|
36
31
|
"access": "public",
|
|
37
32
|
"provenance": true
|
|
@@ -41,41 +36,23 @@
|
|
|
41
36
|
"zod": "^4.4.3"
|
|
42
37
|
},
|
|
43
38
|
"devDependencies": {
|
|
44
|
-
"
|
|
45
|
-
"@changesets/cli": "^2.31.0",
|
|
46
|
-
"@types/node": "^25.9.1",
|
|
47
|
-
"@typescript/native-preview": "7.0.0-dev.20260527.1",
|
|
48
|
-
"husky": "^9.1.7",
|
|
49
|
-
"oxfmt": "^0.52.0",
|
|
50
|
-
"oxlint": "^1.67.0",
|
|
51
|
-
"rolldown": "1.0.2",
|
|
52
|
-
"typescript": "^6.0.3",
|
|
53
|
-
"vitest": "^4.1.7"
|
|
39
|
+
"byterover-cli": "3.16.1"
|
|
54
40
|
},
|
|
55
41
|
"peerDependencies": {
|
|
56
42
|
"@earendil-works/pi-ai": "*",
|
|
57
43
|
"@earendil-works/pi-coding-agent": "*",
|
|
58
44
|
"typebox": "*"
|
|
59
45
|
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22.19.0"
|
|
48
|
+
},
|
|
60
49
|
"pi": {
|
|
61
50
|
"extensions": [
|
|
62
|
-
"./
|
|
51
|
+
"./src/index.ts"
|
|
63
52
|
]
|
|
64
53
|
},
|
|
65
54
|
"scripts": {
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"build:types": "tsgo --project tsconfig.build.json --emitDeclarationOnly",
|
|
69
|
-
"build:watch": "rolldown -c -w",
|
|
70
|
-
"clean": "rm -rf dist",
|
|
71
|
-
"changeset": "changeset",
|
|
72
|
-
"format": "oxfmt",
|
|
73
|
-
"format:check": "oxfmt --check",
|
|
74
|
-
"lint": "oxlint",
|
|
75
|
-
"lint:fix": "oxlint --fix",
|
|
76
|
-
"release": "pnpm run build && changeset publish",
|
|
77
|
-
"test": "vitest run",
|
|
78
|
-
"typecheck": "tsgo --noEmit",
|
|
79
|
-
"version-packages": "changeset version"
|
|
55
|
+
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
56
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
80
57
|
}
|
|
81
58
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type * as z from "zod/v4";
|
|
5
|
+
import { ConfigSchema } from "./config.js";
|
|
6
|
+
|
|
7
|
+
export type ByteroverConfig = z.infer<typeof ConfigSchema>;
|
|
8
|
+
|
|
9
|
+
export type LoadConfigOptions = {
|
|
10
|
+
cwd: string;
|
|
11
|
+
homeDir?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type LoadConfigResult =
|
|
15
|
+
| { success: true; config: ByteroverConfig; source?: string }
|
|
16
|
+
| { success: false; source: string; error: Error };
|
|
17
|
+
|
|
18
|
+
const hasCode = (error: unknown, code: string) => {
|
|
19
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const errorMessage = (error: unknown) => {
|
|
23
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const invalidConfig = (source: string, error: unknown): LoadConfigResult => ({
|
|
27
|
+
success: false,
|
|
28
|
+
source,
|
|
29
|
+
error: new Error(`Invalid Byterover configuration in ${source}: ${errorMessage(error)}`),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const loadConfig = async ({
|
|
33
|
+
cwd,
|
|
34
|
+
homeDir = homedir(),
|
|
35
|
+
}: LoadConfigOptions): Promise<LoadConfigResult> => {
|
|
36
|
+
const candidates = [
|
|
37
|
+
join(cwd, ".pi", "byterover.json"),
|
|
38
|
+
join(homeDir, ".pi", "agent", "byterover.json"),
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const source of candidates) {
|
|
42
|
+
let raw: string;
|
|
43
|
+
try {
|
|
44
|
+
raw = await readFile(source, "utf8");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (hasCode(error, "ENOENT")) continue;
|
|
47
|
+
return invalidConfig(source, error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
return { success: true, source, config: ConfigSchema.parse(JSON.parse(raw)) };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return invalidConfig(source, error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { success: true, config: ConfigSchema.parse(undefined) };
|
|
58
|
+
};
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
|
|
3
|
+
export const brvGitignoreBeginMarker = "# BEGIN pi-byterover";
|
|
4
|
+
export const brvGitignoreEndMarker = "# END pi-byterover";
|
|
5
|
+
|
|
6
|
+
export const brvGitignoreRules = `# Dream state and logs
|
|
7
|
+
dream-log/
|
|
8
|
+
dream-state.json
|
|
9
|
+
dream.lock
|
|
10
|
+
|
|
11
|
+
# Review backups
|
|
12
|
+
review-backups/
|
|
13
|
+
|
|
14
|
+
# Generated files
|
|
15
|
+
config.json
|
|
16
|
+
_queue_status.json
|
|
17
|
+
.snapshot.json
|
|
18
|
+
_manifest.json
|
|
19
|
+
_index.md
|
|
20
|
+
*.abstract.md
|
|
21
|
+
*.overview.md
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const brvGitignore = `${brvGitignoreBeginMarker}\n${brvGitignoreRules}${brvGitignoreEndMarker}\n`;
|
|
25
|
+
|
|
26
|
+
export const configDefaults = {
|
|
27
|
+
enabled: true,
|
|
28
|
+
brvPath: "brv",
|
|
29
|
+
searchTimeoutMs: 30_000,
|
|
30
|
+
recallTimeoutMs: 30_000,
|
|
31
|
+
persistTimeoutMs: 60_000,
|
|
32
|
+
quiet: false,
|
|
33
|
+
autoRecall: true,
|
|
34
|
+
autoPersist: true,
|
|
35
|
+
manualTools: true,
|
|
36
|
+
contextTagName: "byterover-context",
|
|
37
|
+
recallPrompt:
|
|
38
|
+
`Recall any relevant context that would help answer the latest user message.\n` +
|
|
39
|
+
`Use the recent conversation only to resolve references and intent.\n` +
|
|
40
|
+
`Do not restate the query in your findings.`,
|
|
41
|
+
persistPrompt:
|
|
42
|
+
`The following is a conversation between a user and an AI assistant.\n` +
|
|
43
|
+
`Curate only information with lasting value: facts, decisions, technical details, preferences, or notable outcomes.\n` +
|
|
44
|
+
`Skip trivial messages such as greetings, acknowledgments ("ok", "thanks", "sure", "got it"), one-word replies, anything with no substantive content.`,
|
|
45
|
+
maxRecallTurns: 3,
|
|
46
|
+
maxRecallChars: 4096,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const maxCuratedTurnCacheSize = 500;
|
|
50
|
+
|
|
51
|
+
const positiveInteger = () => z.number().int().positive();
|
|
52
|
+
const nonEmptyString = () => z.string().trim().min(1);
|
|
53
|
+
|
|
54
|
+
export const ConfigSchema = z
|
|
55
|
+
.object({
|
|
56
|
+
enabled: z.boolean().default(configDefaults.enabled),
|
|
57
|
+
// BrvBridge options
|
|
58
|
+
brvPath: nonEmptyString().optional().default(configDefaults.brvPath),
|
|
59
|
+
searchTimeoutMs: positiveInteger().default(configDefaults.searchTimeoutMs),
|
|
60
|
+
recallTimeoutMs: positiveInteger().default(configDefaults.recallTimeoutMs),
|
|
61
|
+
persistTimeoutMs: positiveInteger().default(configDefaults.persistTimeoutMs),
|
|
62
|
+
// Plugin options
|
|
63
|
+
quiet: z.boolean().default(configDefaults.quiet),
|
|
64
|
+
autoRecall: z.boolean().default(configDefaults.autoRecall),
|
|
65
|
+
autoPersist: z.boolean().default(configDefaults.autoPersist),
|
|
66
|
+
manualTools: z.boolean().default(configDefaults.manualTools),
|
|
67
|
+
contextTagName: nonEmptyString()
|
|
68
|
+
.regex(/^[A-Za-z][A-Za-z0-9._-]*$/u)
|
|
69
|
+
.default(configDefaults.contextTagName),
|
|
70
|
+
recallPrompt: nonEmptyString().default(configDefaults.recallPrompt),
|
|
71
|
+
persistPrompt: nonEmptyString().default(configDefaults.persistPrompt),
|
|
72
|
+
maxRecallTurns: positiveInteger().default(configDefaults.maxRecallTurns),
|
|
73
|
+
maxRecallChars: positiveInteger().default(configDefaults.maxRecallChars),
|
|
74
|
+
})
|
|
75
|
+
.optional()
|
|
76
|
+
.default(configDefaults);
|
package/src/gitignore.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
brvGitignore,
|
|
5
|
+
brvGitignoreBeginMarker,
|
|
6
|
+
brvGitignoreEndMarker,
|
|
7
|
+
brvGitignoreRules,
|
|
8
|
+
} from "./config.js";
|
|
9
|
+
|
|
10
|
+
const hasCode = (error: unknown, code: string) => {
|
|
11
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const escapeRegExp = (value: string) => {
|
|
15
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const managedGitignoreRules = new Set(
|
|
19
|
+
brvGitignoreRules.split("\n").filter((line) => line.length > 0 && !line.startsWith("#")),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const managedGitignoreBlock = new RegExp(
|
|
23
|
+
`(?:^|\\r?\\n)${escapeRegExp(brvGitignoreBeginMarker)}[\\s\\S]*?${escapeRegExp(
|
|
24
|
+
brvGitignoreEndMarker,
|
|
25
|
+
)}\\r?\\n?`,
|
|
26
|
+
"gu",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const normalizeBrvGitignore = (existing: string) => {
|
|
30
|
+
const output: Array<string> = [];
|
|
31
|
+
let insertedManagedBlock = false;
|
|
32
|
+
let skippingManagedBlock = false;
|
|
33
|
+
|
|
34
|
+
const insertManagedBlock = () => {
|
|
35
|
+
if (insertedManagedBlock) return;
|
|
36
|
+
if (output.length > 0 && output[output.length - 1] !== "") output.push("");
|
|
37
|
+
output.push(...brvGitignore.trimEnd().split("\n"));
|
|
38
|
+
insertedManagedBlock = true;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (const line of existing
|
|
42
|
+
.replace(managedGitignoreBlock, (match) => {
|
|
43
|
+
const separator = match.startsWith("\r\n") ? "\r\n" : match.startsWith("\n") ? "\n" : "";
|
|
44
|
+
return `${separator}${brvGitignore}`;
|
|
45
|
+
})
|
|
46
|
+
.split(/\r?\n/)) {
|
|
47
|
+
if (line === brvGitignoreBeginMarker) {
|
|
48
|
+
insertManagedBlock();
|
|
49
|
+
skippingManagedBlock = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (skippingManagedBlock) {
|
|
53
|
+
if (line === brvGitignoreEndMarker) skippingManagedBlock = false;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (line === "# ByteRover generated files" || managedGitignoreRules.has(line)) {
|
|
57
|
+
insertManagedBlock();
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
output.push(line);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
while (output.length > 0 && output[output.length - 1] === "") output.pop();
|
|
64
|
+
if (!insertedManagedBlock) insertManagedBlock();
|
|
65
|
+
|
|
66
|
+
return `${output.join("\n")}\n`;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const ensureBrvGitignore = async (cwd: string) => {
|
|
70
|
+
await access(cwd);
|
|
71
|
+
await mkdir(join(cwd, ".brv"), { recursive: true });
|
|
72
|
+
|
|
73
|
+
const gitignorePath = join(cwd, ".brv", ".gitignore");
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const existing = await readFile(gitignorePath, "utf8");
|
|
77
|
+
const normalized = normalizeBrvGitignore(existing);
|
|
78
|
+
if (existing === normalized) return;
|
|
79
|
+
await writeFile(gitignorePath, normalized, "utf8");
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!hasCode(error, "ENOENT")) throw error;
|
|
82
|
+
await writeFile(gitignorePath, brvGitignore, "utf8");
|
|
83
|
+
}
|
|
84
|
+
};
|