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 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--byterover-111827?style=for-the-badge&logo=github" alt="GitHub repository"></a>
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 build
38
- pi -e ./dist/index.js
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",
4
- "description": "Pi ByteRover extension",
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-byterover#readme",
13
+ "homepage": "https://github.com/ian-pascoe/pi-extensions#readme",
12
14
  "bugs": {
13
- "url": "https://github.com/ian-pascoe/pi-byterover/issues"
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-byterover.git"
21
+ "url": "git+https://github.com/ian-pascoe/pi-extensions.git",
22
+ "directory": "packages/pi-byterover"
20
23
  },
21
24
  "files": [
22
- "dist",
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
- "@changesets/changelog-github": "^0.7.0",
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
- "./dist/index.js"
51
+ "./src/index.ts"
63
52
  ]
64
53
  },
65
54
  "scripts": {
66
- "build": "pnpm run clean && pnpm run build:js && pnpm run build:types",
67
- "build:js": "rolldown -c",
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);
@@ -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
+ };