@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.
@@ -0,0 +1,82 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import yaml from "js-yaml";
4
+ /**
5
+ * Resolve the "age" of a context-tree document for the visible recall summary.
6
+ *
7
+ * Resolution order, falling through on any failure:
8
+ * 1. Frontmatter `updatedAt` — standard topic files (post-curate timestamp)
9
+ * 2. Frontmatter `synthesized_at` — synthesis files produced by `brv dream`
10
+ * 3. Frontmatter `createdAt` — older standard files that lack updatedAt
11
+ * 4. File `mtime` — last resort for files without timestamp frontmatter
12
+ * 5. `undefined` — file missing or path is a cross-project shared source
13
+ *
14
+ * Cross-project paths in the form `[alias]:relative/path.md` are not resolved here —
15
+ * they live in another project's `.brv/context-tree/`, which the plugin does not see.
16
+ */
17
+ export function resolveContextTreeAge(projectRoot, relativePath) {
18
+ // Shared-source paths point outside this project tree; we have no access to their mtime.
19
+ if (relativePath.startsWith("["))
20
+ return undefined;
21
+ const absolutePath = join(projectRoot, ".brv", "context-tree", relativePath);
22
+ let mtime;
23
+ try {
24
+ mtime = statSync(absolutePath).mtime;
25
+ }
26
+ catch {
27
+ // File missing or unreadable — no age to surface.
28
+ return undefined;
29
+ }
30
+ const frontmatterAge = readFrontmatterAge(absolutePath);
31
+ return frontmatterAge ?? mtime;
32
+ }
33
+ const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/;
34
+ const TIMESTAMP_KEYS_IN_PRIORITY = [
35
+ "updatedAt",
36
+ "synthesized_at",
37
+ "createdAt",
38
+ ];
39
+ function readFrontmatterAge(absolutePath) {
40
+ let head;
41
+ try {
42
+ // Frontmatter sits at the top of the file; reading the whole file is fine for context-tree
43
+ // documents (typically <50KB) and avoids the complexity of streaming a partial read.
44
+ head = readFileSync(absolutePath, "utf8");
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ const match = head.match(FRONTMATTER_REGEX);
50
+ if (!match)
51
+ return undefined;
52
+ let parsed;
53
+ try {
54
+ parsed = yaml.load(match[1] ?? "");
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ if (!isPlainObject(parsed))
60
+ return undefined;
61
+ for (const key of TIMESTAMP_KEYS_IN_PRIORITY) {
62
+ const candidate = parsed[key];
63
+ const date = coerceDate(candidate);
64
+ if (date)
65
+ return date;
66
+ }
67
+ return undefined;
68
+ }
69
+ function isPlainObject(value) {
70
+ return value !== null && typeof value === "object";
71
+ }
72
+ function coerceDate(value) {
73
+ if (value instanceof Date) {
74
+ return Number.isNaN(value.getTime()) ? undefined : value;
75
+ }
76
+ if (typeof value === "string") {
77
+ const parsed = new Date(value);
78
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed;
79
+ }
80
+ return undefined;
81
+ }
82
+ //# sourceMappingURL=resolve-context-tree-age.js.map
@@ -0,0 +1,31 @@
1
+ export type BrvState = "dreaming" | "curating" | "idle";
2
+ /**
3
+ * Walk up from `startCwd` looking for a `.brv` directory.
4
+ * Returns the absolute path to `.brv` when found, or undefined when no
5
+ * ancestor contains one. Closest match wins.
6
+ */
7
+ export declare function findBrvDir(startCwd: string): string | undefined;
8
+ /**
9
+ * Inspect the daemon's persisted activity and classify the project state.
10
+ * Pure filesystem inspection — no daemon RPC.
11
+ *
12
+ * Two storage locations are checked because the daemon writes them in
13
+ * different baseDirs (project-local vs global):
14
+ * - dream-log + dream.lock → `<brvDir>/dream-log/`, `<brvDir>/dream.lock`
15
+ * - curate-log → `<getProjectDataDir(cwd)>/curate-log/`
16
+ *
17
+ * Precedence (confident signals first, weak signals last):
18
+ * 1. dream-log latest === "processing" → dreaming (confident)
19
+ * 2. curate-log latest === "processing" → curating (confident)
20
+ * 3. dream.lock present, fresh, log isn't "completed" → dreaming (weak fallback)
21
+ * 4. otherwise → idle
22
+ *
23
+ * Confident signals (active log entries) outrank the weak dream.lock fallback
24
+ * so that an active curate isn't masked by a stale dream.lock. Real concurrent
25
+ * dream + curate is preserved (rule 1 wins). Stale-lock false-positives are
26
+ * bounded by `STALE_LOCK_THRESHOLD_MS`.
27
+ *
28
+ * `cwd` is needed to resolve the daemon's per-project storage directory; it
29
+ * defaults to the parent of `brvDir` (the project root).
30
+ */
31
+ export declare function detectState(brvDir: string, cwd?: string): BrvState;
@@ -0,0 +1,123 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { getProjectDataDir } from "./project-data-dir.js";
4
+ /**
5
+ * Maximum age we will trust the standalone `dream.lock` signal. Beyond this
6
+ * the lock is treated as stale and discarded — the daemon has been observed
7
+ * to leave locks behind on abnormal exit, so a very old lock is more likely
8
+ * to be a leftover than a still-running dream.
9
+ *
10
+ * Set to 15 minutes: comfortably above the longest dream observed in this
11
+ * project's logs (~10 min) plus headroom for slower providers / bigger
12
+ * trees, but short enough that a stuck false-positive self-heals within a
13
+ * reasonable window. Tune later if real dreams exceed this on other projects.
14
+ */
15
+ const STALE_LOCK_THRESHOLD_MS = 15 * 60 * 1000;
16
+ /**
17
+ * Walk up from `startCwd` looking for a `.brv` directory.
18
+ * Returns the absolute path to `.brv` when found, or undefined when no
19
+ * ancestor contains one. Closest match wins.
20
+ */
21
+ export function findBrvDir(startCwd) {
22
+ let dir = startCwd;
23
+ while (true) {
24
+ const candidate = join(dir, ".brv");
25
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
26
+ return candidate;
27
+ }
28
+ const parent = dirname(dir);
29
+ if (parent === dir)
30
+ return undefined;
31
+ dir = parent;
32
+ }
33
+ }
34
+ /**
35
+ * Inspect the daemon's persisted activity and classify the project state.
36
+ * Pure filesystem inspection — no daemon RPC.
37
+ *
38
+ * Two storage locations are checked because the daemon writes them in
39
+ * different baseDirs (project-local vs global):
40
+ * - dream-log + dream.lock → `<brvDir>/dream-log/`, `<brvDir>/dream.lock`
41
+ * - curate-log → `<getProjectDataDir(cwd)>/curate-log/`
42
+ *
43
+ * Precedence (confident signals first, weak signals last):
44
+ * 1. dream-log latest === "processing" → dreaming (confident)
45
+ * 2. curate-log latest === "processing" → curating (confident)
46
+ * 3. dream.lock present, fresh, log isn't "completed" → dreaming (weak fallback)
47
+ * 4. otherwise → idle
48
+ *
49
+ * Confident signals (active log entries) outrank the weak dream.lock fallback
50
+ * so that an active curate isn't masked by a stale dream.lock. Real concurrent
51
+ * dream + curate is preserved (rule 1 wins). Stale-lock false-positives are
52
+ * bounded by `STALE_LOCK_THRESHOLD_MS`.
53
+ *
54
+ * `cwd` is needed to resolve the daemon's per-project storage directory; it
55
+ * defaults to the parent of `brvDir` (the project root).
56
+ */
57
+ export function detectState(brvDir, cwd) {
58
+ const lockPath = join(brvDir, "dream.lock");
59
+ const lockExists = existsSync(lockPath);
60
+ const latestDream = latestLogStatus(join(brvDir, "dream-log"));
61
+ // Rule 1 — confident dream signal
62
+ if (latestDream === "processing")
63
+ return "dreaming";
64
+ // Rule 2 — confident curate signal, beats the weak dream.lock fallback
65
+ const projectCwd = cwd ?? dirname(brvDir);
66
+ let projectDataDir;
67
+ try {
68
+ projectDataDir = getProjectDataDir(projectCwd);
69
+ }
70
+ catch {
71
+ projectDataDir = undefined;
72
+ }
73
+ if (projectDataDir !== undefined) {
74
+ if (latestLogStatus(join(projectDataDir, "curate-log")) === "processing") {
75
+ return "curating";
76
+ }
77
+ }
78
+ // Rule 3 — dream.lock fallback. Honored only when:
79
+ // (a) latest dream-log isn't already "completed" (cross-check)
80
+ // (b) lock is fresh (within STALE_LOCK_THRESHOLD_MS)
81
+ if (lockExists && latestDream !== "completed" && lockIsFresh(lockPath)) {
82
+ return "dreaming";
83
+ }
84
+ return "idle";
85
+ }
86
+ function lockIsFresh(lockPath) {
87
+ try {
88
+ const stat = statSync(lockPath);
89
+ return Date.now() - stat.mtimeMs < STALE_LOCK_THRESHOLD_MS;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ /** Returns the `status` field of the lex-most-recent *.json log in `logDir`. */
96
+ function latestLogStatus(logDir) {
97
+ let entries;
98
+ try {
99
+ entries = readdirSync(logDir);
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ const jsonFiles = entries.filter((e) => e.endsWith(".json")).sort();
105
+ const latest = jsonFiles.at(-1);
106
+ if (latest === undefined)
107
+ return undefined;
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(readFileSync(join(logDir, latest), "utf8"));
111
+ }
112
+ catch {
113
+ return undefined;
114
+ }
115
+ if (typeof parsed === "object" &&
116
+ parsed !== null &&
117
+ "status" in parsed &&
118
+ typeof parsed.status === "string") {
119
+ return parsed.status;
120
+ }
121
+ return undefined;
122
+ }
123
+ //# sourceMappingURL=state-detector.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byterover/claude-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
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
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,8 @@
23
23
  "LICENSE"
24
24
  ],
25
25
  "dependencies": {
26
- "@byterover/brv-bridge": "^1.0.2",
26
+ "@byterover/brv-bridge": "^1.2.0",
27
+ "@inquirer/prompts": "^7.0.0",
27
28
  "commander": "^13.0.0",
28
29
  "js-yaml": "^4.1.0",
29
30
  "picocolors": "^1.1.0",