byterover-cli 1.0.3 → 1.0.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.
Files changed (176) hide show
  1. package/README.md +75 -12
  2. package/dist/commands/curate.js +3 -3
  3. package/dist/commands/main.d.ts +13 -0
  4. package/dist/commands/main.js +55 -4
  5. package/dist/commands/query.js +3 -3
  6. package/dist/commands/status.js +2 -2
  7. package/dist/constants.d.ts +2 -1
  8. package/dist/constants.js +4 -1
  9. package/dist/core/domain/cipher/file-system/types.d.ts +2 -0
  10. package/dist/core/domain/cipher/llm/registry.js +53 -2
  11. package/dist/core/domain/cipher/llm/types.d.ts +2 -0
  12. package/dist/core/domain/cipher/process/types.d.ts +7 -0
  13. package/dist/core/domain/cipher/session/session-metadata.d.ts +178 -0
  14. package/dist/core/domain/cipher/session/session-metadata.js +147 -0
  15. package/dist/core/domain/entities/auth-token.js +6 -3
  16. package/dist/core/domain/entities/event.d.ts +1 -1
  17. package/dist/core/domain/entities/event.js +2 -1
  18. package/dist/core/domain/knowledge/markdown-writer.d.ts +15 -18
  19. package/dist/core/domain/knowledge/markdown-writer.js +232 -34
  20. package/dist/core/domain/knowledge/relation-parser.d.ts +37 -36
  21. package/dist/core/domain/knowledge/relation-parser.js +53 -58
  22. package/dist/core/domain/transport/schemas.d.ts +52 -1
  23. package/dist/core/domain/transport/schemas.js +30 -1
  24. package/dist/core/interfaces/cipher/i-blob-storage.d.ts +6 -0
  25. package/dist/core/interfaces/cipher/i-session-persistence.d.ts +133 -0
  26. package/dist/core/interfaces/cipher/i-session-persistence.js +7 -0
  27. package/dist/core/interfaces/cipher/index.d.ts +0 -1
  28. package/dist/core/interfaces/cipher/message-types.d.ts +6 -0
  29. package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -0
  30. package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
  31. package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -1
  32. package/dist/infra/cipher/agent/agent-schemas.d.ts +6 -6
  33. package/dist/infra/cipher/agent/cipher-agent.js +4 -0
  34. package/dist/infra/cipher/agent/service-initializer.js +4 -4
  35. package/dist/infra/cipher/file-system/context-tree-file-system-factory.js +3 -2
  36. package/dist/infra/cipher/file-system/file-system-service.d.ts +4 -0
  37. package/dist/infra/cipher/file-system/file-system-service.js +6 -0
  38. package/dist/infra/cipher/http/internal-llm-http-service.js +3 -5
  39. package/dist/infra/cipher/interactive-loop.js +3 -1
  40. package/dist/infra/cipher/llm/context/context-manager.js +40 -16
  41. package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
  42. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +98 -6
  43. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -2
  44. package/dist/infra/cipher/llm/thought-parser.d.ts +21 -0
  45. package/dist/infra/cipher/llm/thought-parser.js +27 -0
  46. package/dist/infra/cipher/llm/tool-output-processor.d.ts +10 -0
  47. package/dist/infra/cipher/llm/tool-output-processor.js +80 -7
  48. package/dist/infra/cipher/process/process-service.js +11 -3
  49. package/dist/infra/cipher/session/chat-session.d.ts +7 -2
  50. package/dist/infra/cipher/session/chat-session.js +90 -52
  51. package/dist/infra/cipher/session/session-metadata-store.d.ts +52 -0
  52. package/dist/infra/cipher/session/session-metadata-store.js +406 -0
  53. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +4 -2
  54. package/dist/infra/cipher/tools/implementations/create-knowledge-topic-tool.js +24 -17
  55. package/dist/infra/cipher/tools/implementations/curate-tool.js +138 -65
  56. package/dist/infra/cipher/tools/implementations/read-file-tool.js +3 -12
  57. package/dist/infra/cipher/tools/implementations/spec-analyze-tool.js +18 -15
  58. package/dist/infra/cipher/tools/implementations/task-tool.js +54 -7
  59. package/dist/infra/context-tree/file-context-file-reader.js +4 -0
  60. package/dist/infra/context-tree/file-context-tree-service.js +4 -15
  61. package/dist/infra/core/executors/curate-executor.d.ts +2 -7
  62. package/dist/infra/core/executors/curate-executor.js +18 -53
  63. package/dist/infra/core/executors/query-executor.d.ts +1 -7
  64. package/dist/infra/core/executors/query-executor.js +10 -35
  65. package/dist/infra/core/task-processor.d.ts +2 -0
  66. package/dist/infra/core/task-processor.js +1 -0
  67. package/dist/infra/http/authenticated-http-client.js +5 -0
  68. package/dist/infra/process/agent-worker.js +113 -6
  69. package/dist/infra/process/constants.d.ts +1 -0
  70. package/dist/infra/process/constants.js +1 -0
  71. package/dist/infra/process/process-manager.d.ts +10 -1
  72. package/dist/infra/process/process-manager.js +16 -6
  73. package/dist/infra/process/task-queue-manager.js +2 -1
  74. package/dist/infra/process/transport-handlers.js +35 -0
  75. package/dist/infra/process/transport-worker.js +89 -1
  76. package/dist/infra/repl/commands/curate-command.js +2 -2
  77. package/dist/infra/repl/commands/gen-rules-command.js +2 -2
  78. package/dist/infra/repl/commands/index.js +5 -2
  79. package/dist/infra/repl/commands/init-command.js +2 -2
  80. package/dist/infra/repl/commands/login-command.js +2 -2
  81. package/dist/infra/repl/commands/logout-command.js +2 -2
  82. package/dist/infra/repl/commands/new-command.d.ts +14 -0
  83. package/dist/infra/repl/commands/new-command.js +61 -0
  84. package/dist/infra/repl/commands/pull-command.js +2 -2
  85. package/dist/infra/repl/commands/push-command.js +2 -2
  86. package/dist/infra/repl/commands/query-command.js +2 -2
  87. package/dist/infra/repl/commands/{clear-command.d.ts → reset-command.d.ts} +2 -2
  88. package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +10 -10
  89. package/dist/infra/repl/commands/space/list-command.js +2 -2
  90. package/dist/infra/repl/commands/space/switch-command.js +2 -2
  91. package/dist/infra/repl/commands/status-command.js +2 -2
  92. package/dist/infra/repl/repl-startup.js +0 -2
  93. package/dist/infra/storage/file-token-store.d.ts +31 -0
  94. package/dist/infra/storage/file-token-store.js +98 -0
  95. package/dist/infra/storage/keychain-token-store.d.ts +4 -1
  96. package/dist/infra/storage/keychain-token-store.js +6 -4
  97. package/dist/infra/storage/token-store.d.ts +10 -0
  98. package/dist/infra/storage/token-store.js +14 -0
  99. package/dist/infra/usecase/curate-use-case.js +1 -1
  100. package/dist/infra/usecase/generate-rules-use-case.js +2 -2
  101. package/dist/infra/usecase/init-use-case.js +4 -4
  102. package/dist/infra/usecase/logout-use-case.js +1 -1
  103. package/dist/infra/usecase/push-use-case.js +1 -1
  104. package/dist/infra/usecase/{clear-use-case.d.ts → reset-use-case.d.ts} +5 -5
  105. package/dist/infra/usecase/{clear-use-case.js → reset-use-case.js} +5 -5
  106. package/dist/infra/user/http-user-service.js +6 -11
  107. package/dist/resources/prompts/curate.yml +79 -15
  108. package/dist/resources/prompts/plan.yml +6 -0
  109. package/dist/resources/tools/curate.txt +60 -15
  110. package/dist/tui/app.js +1 -1
  111. package/dist/tui/components/execution/log-item.js +2 -5
  112. package/dist/tui/components/header.d.ts +1 -1
  113. package/dist/tui/components/header.js +25 -4
  114. package/dist/tui/components/index.d.ts +5 -1
  115. package/dist/tui/components/index.js +3 -1
  116. package/dist/tui/components/init.d.ts +33 -0
  117. package/dist/tui/components/init.js +253 -0
  118. package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
  119. package/dist/tui/components/onboarding/index.d.ts +1 -0
  120. package/dist/tui/components/onboarding/index.js +1 -0
  121. package/dist/tui/components/onboarding/onboarding-flow.d.ts +2 -0
  122. package/dist/tui/components/onboarding/onboarding-flow.js +9 -229
  123. package/dist/tui/components/onboarding/onboarding-step.js +1 -1
  124. package/dist/tui/components/onboarding/welcome-box.d.ts +14 -0
  125. package/dist/tui/components/onboarding/welcome-box.js +23 -0
  126. package/dist/tui/components/status-badge.d.ts +22 -0
  127. package/dist/tui/components/status-badge.js +32 -0
  128. package/dist/tui/contexts/auth-context.js +2 -1
  129. package/dist/tui/contexts/index.d.ts +1 -0
  130. package/dist/tui/contexts/index.js +1 -0
  131. package/dist/tui/contexts/onboarding-context.d.ts +14 -0
  132. package/dist/tui/contexts/onboarding-context.js +17 -22
  133. package/dist/tui/contexts/status-context.d.ts +33 -0
  134. package/dist/tui/contexts/status-context.js +159 -0
  135. package/dist/tui/hooks/use-auth-polling.d.ts +4 -1
  136. package/dist/tui/hooks/use-auth-polling.js +21 -7
  137. package/dist/tui/hooks/use-tab-navigation.js +0 -2
  138. package/dist/tui/providers/app-providers.js +2 -2
  139. package/dist/tui/types/index.d.ts +2 -0
  140. package/dist/tui/types/index.js +2 -0
  141. package/dist/tui/types/status.d.ts +46 -0
  142. package/dist/tui/types/status.js +13 -0
  143. package/dist/tui/utils/index.d.ts +6 -0
  144. package/dist/tui/utils/index.js +6 -0
  145. package/dist/tui/utils/time.d.ts +10 -0
  146. package/dist/tui/utils/time.js +15 -0
  147. package/dist/tui/views/command-view.js +15 -2
  148. package/dist/tui/views/index.d.ts +1 -0
  149. package/dist/tui/views/index.js +1 -0
  150. package/dist/tui/views/init-view.d.ts +15 -0
  151. package/dist/tui/views/init-view.js +29 -0
  152. package/dist/tui/views/logs-view.js +22 -8
  153. package/dist/utils/environment-detector.d.ts +5 -0
  154. package/dist/utils/environment-detector.js +31 -0
  155. package/dist/utils/file-validator.js +9 -7
  156. package/dist/utils/global-data-path.d.ts +11 -0
  157. package/dist/utils/global-data-path.js +32 -0
  158. package/oclif.manifest.json +3 -3
  159. package/package.json +1 -1
  160. package/dist/config/context-tree-domains.d.ts +0 -17
  161. package/dist/config/context-tree-domains.js +0 -34
  162. package/dist/core/interfaces/cipher/i-agent-storage.d.ts +0 -152
  163. package/dist/core/interfaces/usecase/i-clear-use-case.js +0 -1
  164. package/dist/infra/cipher/consumer/consumer-lock.d.ts +0 -20
  165. package/dist/infra/cipher/consumer/consumer-lock.js +0 -41
  166. package/dist/infra/cipher/consumer/consumer-service.d.ts +0 -99
  167. package/dist/infra/cipher/consumer/consumer-service.js +0 -166
  168. package/dist/infra/cipher/consumer/execution-consumer.d.ts +0 -126
  169. package/dist/infra/cipher/consumer/execution-consumer.js +0 -561
  170. package/dist/infra/cipher/consumer/index.d.ts +0 -33
  171. package/dist/infra/cipher/consumer/index.js +0 -34
  172. package/dist/infra/cipher/consumer/queue-polling-service.d.ts +0 -120
  173. package/dist/infra/cipher/consumer/queue-polling-service.js +0 -249
  174. package/dist/infra/cipher/storage/agent-storage.d.ts +0 -246
  175. package/dist/infra/cipher/storage/agent-storage.js +0 -956
  176. /package/dist/core/interfaces/{cipher/i-agent-storage.js → usecase/i-reset-use-case.js} +0 -0
@@ -0,0 +1,406 @@
1
+ /**
2
+ * SessionMetadataStore - Manages session metadata persistence.
3
+ *
4
+ * Stores session metadata in .brv/sessions/ directory:
5
+ * - active.json: Current active session pointer
6
+ * - session-*.json: Individual session metadata files
7
+ *
8
+ * Design adapted from gemini-cli's ChatRecordingService pattern.
9
+ */
10
+ import { randomUUID } from 'node:crypto';
11
+ import * as fs from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+ import { ACTIVE_SESSION_FILE, ActiveSessionPointerSchema, cleanMessageForTitle, generateSessionFilename, parseSessionFilename, SESSION_FILE_PREFIX, SessionMetadataSchema, SESSIONS_DIR, } from '../../../core/domain/cipher/session/session-metadata.js';
14
+ /**
15
+ * Check if a process with given PID is running.
16
+ *
17
+ * @param pid - Process ID to check
18
+ * @returns True if process is running
19
+ */
20
+ function isProcessRunning(pid) {
21
+ try {
22
+ // Sending signal 0 checks if process exists without actually sending a signal
23
+ process.kill(pid, 0);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Session ID prefix used in the application.
32
+ */
33
+ const SESSION_ID_PREFIX = 'agent-session-';
34
+ /**
35
+ * Unique token for this process instance.
36
+ * Used to detect PID reuse: different process instance = different token,
37
+ * even if the OS assigned the same PID after the original process crashed.
38
+ */
39
+ const PROCESS_TOKEN = randomUUID();
40
+ /**
41
+ * Extract the UUID portion from a session ID.
42
+ *
43
+ * Session IDs have format: "agent-session-<UUID>" or just "<UUID>"
44
+ * This function handles both formats for backward compatibility.
45
+ *
46
+ * @param sessionId - The full session ID
47
+ * @returns The UUID portion without the prefix
48
+ */
49
+ function extractUuidFromSessionId(sessionId) {
50
+ return sessionId.startsWith(SESSION_ID_PREFIX)
51
+ ? sessionId.slice(SESSION_ID_PREFIX.length)
52
+ : sessionId;
53
+ }
54
+ /**
55
+ * SessionMetadataStore implementation.
56
+ *
57
+ * Manages session metadata stored in .brv/sessions/ directory.
58
+ */
59
+ export class SessionMetadataStore {
60
+ activeSessionPath;
61
+ sessionsDir;
62
+ workingDirectory;
63
+ /**
64
+ * Create a new SessionMetadataStore.
65
+ *
66
+ * @param workingDirectory - Project working directory (defaults to process.cwd())
67
+ */
68
+ constructor(workingDirectory) {
69
+ this.workingDirectory = workingDirectory ?? process.cwd();
70
+ this.sessionsDir = join(this.workingDirectory, '.brv', SESSIONS_DIR);
71
+ this.activeSessionPath = join(this.sessionsDir, ACTIVE_SESSION_FILE);
72
+ }
73
+ // ============================================================================
74
+ // Active Session Management
75
+ // ============================================================================
76
+ async cleanupSessions(config) {
77
+ const result = {
78
+ corruptedRemoved: 0,
79
+ deletedByAge: 0,
80
+ deletedByCount: 0,
81
+ remaining: 0,
82
+ };
83
+ try {
84
+ const files = await fs.readdir(this.sessionsDir);
85
+ const sessionFiles = files.filter((f) => f.startsWith(SESSION_FILE_PREFIX) && f.endsWith('.json'));
86
+ // IMPORTANT: Capture the active session ID as an immutable primitive BEFORE
87
+ // any file mutations. This prevents race conditions where concurrent cleanup
88
+ // operations could modify the active session pointer while we're iterating.
89
+ // Using a primitive string (not object reference) ensures we have a stable
90
+ // value to check against throughout the entire cleanup operation.
91
+ const active = await this.getActiveSession();
92
+ const activeSessionId = active?.sessionId;
93
+ const validSessions = [];
94
+ // First pass: identify corrupted files and valid sessions
95
+ for (const file of sessionFiles) {
96
+ const filePath = join(this.sessionsDir, file);
97
+ try {
98
+ // eslint-disable-next-line no-await-in-loop
99
+ const content = await fs.readFile(filePath, 'utf8');
100
+ const data = JSON.parse(content);
101
+ const parseResult = SessionMetadataSchema.safeParse(data);
102
+ if (!parseResult.success) {
103
+ // Corrupted file - delete it
104
+ // eslint-disable-next-line no-await-in-loop
105
+ await fs.unlink(filePath);
106
+ result.corruptedRemoved++;
107
+ continue;
108
+ }
109
+ validSessions.push({ file, metadata: parseResult.data });
110
+ }
111
+ catch {
112
+ // Can't read/parse - delete it
113
+ try {
114
+ // eslint-disable-next-line no-await-in-loop
115
+ await fs.unlink(filePath);
116
+ result.corruptedRemoved++;
117
+ }
118
+ catch {
119
+ // Ignore delete errors
120
+ }
121
+ }
122
+ }
123
+ // Sort by lastUpdated (newest first)
124
+ validSessions.sort((a, b) => new Date(b.metadata.lastUpdated).getTime() - new Date(a.metadata.lastUpdated).getTime());
125
+ const now = Date.now();
126
+ const maxAgeMs = config.maxAgeDays * 24 * 60 * 60 * 1000;
127
+ // Second pass: apply retention policies
128
+ for (const [i, { file, metadata }] of validSessions.entries()) {
129
+ // Never delete the current active session (uses captured primitive ID)
130
+ if (activeSessionId && metadata.sessionId === activeSessionId) {
131
+ continue;
132
+ }
133
+ const age = now - new Date(metadata.lastUpdated).getTime();
134
+ const shouldDeleteByAge = age > maxAgeMs;
135
+ const shouldDeleteByCount = i >= config.maxCount;
136
+ const shouldDelete = shouldDeleteByAge || shouldDeleteByCount;
137
+ if (!shouldDelete) {
138
+ continue;
139
+ }
140
+ try {
141
+ // eslint-disable-next-line no-await-in-loop
142
+ await fs.unlink(join(this.sessionsDir, file));
143
+ if (shouldDeleteByAge) {
144
+ result.deletedByAge++;
145
+ }
146
+ else {
147
+ result.deletedByCount++;
148
+ }
149
+ }
150
+ catch {
151
+ // Ignore delete errors
152
+ }
153
+ }
154
+ // Count remaining
155
+ const remainingFiles = await fs.readdir(this.sessionsDir);
156
+ result.remaining = remainingFiles.filter((f) => f.startsWith(SESSION_FILE_PREFIX) && f.endsWith('.json')).length;
157
+ return result;
158
+ }
159
+ catch (error) {
160
+ if (error.code === 'ENOENT') {
161
+ return result;
162
+ }
163
+ throw error;
164
+ }
165
+ }
166
+ async clearActiveSession() {
167
+ try {
168
+ await fs.unlink(this.activeSessionPath);
169
+ }
170
+ catch (error) {
171
+ // Ignore if file doesn't exist
172
+ if (error.code !== 'ENOENT') {
173
+ throw error;
174
+ }
175
+ }
176
+ }
177
+ /**
178
+ * Create a new session metadata object.
179
+ *
180
+ * @param sessionId - Session ID
181
+ * @returns New session metadata with defaults
182
+ */
183
+ createSessionMetadata(sessionId) {
184
+ const now = new Date().toISOString();
185
+ return {
186
+ createdAt: now,
187
+ lastUpdated: now,
188
+ messageCount: 0,
189
+ sessionId,
190
+ status: 'active',
191
+ workingDirectory: this.workingDirectory,
192
+ };
193
+ }
194
+ async deleteSession(sessionId) {
195
+ try {
196
+ const files = await fs.readdir(this.sessionsDir);
197
+ for (const file of files) {
198
+ if (!file.startsWith(SESSION_FILE_PREFIX) || !file.endsWith('.json')) {
199
+ continue;
200
+ }
201
+ const parsed = parseSessionFilename(file);
202
+ // Extract UUID from sessionId (removes "agent-session-" prefix if present)
203
+ // then compare with the filename's uuid prefix
204
+ const uuid = extractUuidFromSessionId(sessionId);
205
+ if (parsed && uuid.startsWith(parsed.uuidPrefix)) {
206
+ // eslint-disable-next-line no-await-in-loop
207
+ await fs.unlink(join(this.sessionsDir, file));
208
+ return true;
209
+ }
210
+ // Also check by reading the file to match full sessionId
211
+ try {
212
+ const filePath = join(this.sessionsDir, file);
213
+ // eslint-disable-next-line no-await-in-loop
214
+ const content = await fs.readFile(filePath, 'utf8');
215
+ const data = JSON.parse(content);
216
+ if (data.sessionId === sessionId) {
217
+ // eslint-disable-next-line no-await-in-loop
218
+ await fs.unlink(filePath);
219
+ return true;
220
+ }
221
+ }
222
+ catch {
223
+ // Continue to next file
224
+ }
225
+ }
226
+ return false;
227
+ }
228
+ catch (error) {
229
+ if (error.code === 'ENOENT') {
230
+ return false;
231
+ }
232
+ throw error;
233
+ }
234
+ }
235
+ async getActiveSession() {
236
+ try {
237
+ const content = await fs.readFile(this.activeSessionPath, 'utf8');
238
+ const data = JSON.parse(content);
239
+ const result = ActiveSessionPointerSchema.safeParse(data);
240
+ if (!result.success) {
241
+ // Invalid format - treat as no active session
242
+ return null;
243
+ }
244
+ return result.data;
245
+ }
246
+ catch (error) {
247
+ // File doesn't exist or can't be read
248
+ if (error.code === 'ENOENT') {
249
+ return null;
250
+ }
251
+ throw error;
252
+ }
253
+ }
254
+ async getSession(sessionId) {
255
+ const sessions = await this.listSessions();
256
+ return sessions.find((s) => s.sessionId === sessionId) ?? null;
257
+ }
258
+ async isActiveSessionStale() {
259
+ const active = await this.getActiveSession();
260
+ if (!active) {
261
+ return false;
262
+ }
263
+ // If the process is not running, the session is definitely stale
264
+ if (!isProcessRunning(active.pid)) {
265
+ return true;
266
+ }
267
+ // If process is running but token is missing or doesn't match,
268
+ // it's either an old session file or a different process with the same PID.
269
+ // Both cases indicate a stale session.
270
+ if (!active.processToken || active.processToken !== PROCESS_TOKEN) {
271
+ return true;
272
+ }
273
+ return false;
274
+ }
275
+ async isSessionForCurrentProject(sessionId) {
276
+ const session = await this.getSession(sessionId);
277
+ if (!session) {
278
+ return false;
279
+ }
280
+ return session.workingDirectory === this.workingDirectory;
281
+ }
282
+ async listSessions() {
283
+ try {
284
+ await this.ensureSessionsDir();
285
+ const files = await fs.readdir(this.sessionsDir);
286
+ const sessionFiles = files.filter((f) => f.startsWith(SESSION_FILE_PREFIX) && f.endsWith('.json'));
287
+ const active = await this.getActiveSession();
288
+ const sessions = [];
289
+ for (const file of sessionFiles) {
290
+ try {
291
+ const filePath = join(this.sessionsDir, file);
292
+ // eslint-disable-next-line no-await-in-loop
293
+ const content = await fs.readFile(filePath, 'utf8');
294
+ const data = JSON.parse(content);
295
+ const result = SessionMetadataSchema.safeParse(data);
296
+ if (!result.success) {
297
+ // Skip corrupted files
298
+ continue;
299
+ }
300
+ const metadata = result.data;
301
+ const isCurrentSession = active?.sessionId === metadata.sessionId;
302
+ sessions.push({
303
+ ...metadata,
304
+ file: file.replace('.json', ''),
305
+ fileName: file,
306
+ firstUserMessage: metadata.title,
307
+ index: 0, // Will be set after sorting
308
+ isCurrentSession,
309
+ });
310
+ }
311
+ catch {
312
+ // Skip files that can't be read or parsed
313
+ continue;
314
+ }
315
+ }
316
+ // Sort by lastUpdated (newest first)
317
+ sessions.sort((a, b) => new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime());
318
+ // Set 1-based indexes
319
+ for (const [index, session] of sessions.entries()) {
320
+ session.index = index + 1;
321
+ }
322
+ return sessions;
323
+ }
324
+ catch (error) {
325
+ // Directory doesn't exist
326
+ if (error.code === 'ENOENT') {
327
+ return [];
328
+ }
329
+ throw error;
330
+ }
331
+ }
332
+ // ============================================================================
333
+ // Session Lifecycle
334
+ // ============================================================================
335
+ async markSessionEnded(sessionId) {
336
+ const session = await this.getSession(sessionId);
337
+ if (session) {
338
+ session.status = 'ended';
339
+ session.lastUpdated = new Date().toISOString();
340
+ await this.saveSession(session);
341
+ }
342
+ }
343
+ async markSessionInterrupted(sessionId) {
344
+ const session = await this.getSession(sessionId);
345
+ if (session) {
346
+ session.status = 'interrupted';
347
+ session.lastUpdated = new Date().toISOString();
348
+ await this.saveSession(session);
349
+ }
350
+ }
351
+ async saveSession(metadata) {
352
+ await this.ensureSessionsDir();
353
+ // Find existing file for this session or create new
354
+ let filename;
355
+ try {
356
+ const files = await fs.readdir(this.sessionsDir);
357
+ const existingFile = files.find((f) => {
358
+ if (!f.startsWith(SESSION_FILE_PREFIX) || !f.endsWith('.json')) {
359
+ return false;
360
+ }
361
+ const parsed = parseSessionFilename(f);
362
+ // Extract UUID from sessionId and compare with filename's uuid prefix
363
+ const uuid = extractUuidFromSessionId(metadata.sessionId);
364
+ return parsed && uuid.startsWith(parsed.uuidPrefix);
365
+ });
366
+ filename = existingFile ?? generateSessionFilename(metadata.sessionId);
367
+ }
368
+ catch {
369
+ filename = generateSessionFilename(metadata.sessionId);
370
+ }
371
+ const filePath = join(this.sessionsDir, filename);
372
+ await fs.writeFile(filePath, JSON.stringify(metadata, null, 2), 'utf8');
373
+ }
374
+ async setActiveSession(sessionId) {
375
+ await this.ensureSessionsDir();
376
+ const pointer = {
377
+ activatedAt: new Date().toISOString(),
378
+ pid: process.pid,
379
+ processToken: PROCESS_TOKEN,
380
+ sessionId,
381
+ };
382
+ await fs.writeFile(this.activeSessionPath, JSON.stringify(pointer, null, 2), 'utf8');
383
+ }
384
+ async setSessionTitle(sessionId, title) {
385
+ const session = await this.getSession(sessionId);
386
+ if (session && !session.title) {
387
+ session.title = cleanMessageForTitle(title);
388
+ session.lastUpdated = new Date().toISOString();
389
+ await this.saveSession(session);
390
+ }
391
+ }
392
+ async updateSessionActivity(sessionId, messageCount) {
393
+ const session = await this.getSession(sessionId);
394
+ if (session) {
395
+ session.lastUpdated = new Date().toISOString();
396
+ session.messageCount = messageCount;
397
+ await this.saveSession(session);
398
+ }
399
+ }
400
+ /**
401
+ * Ensure the sessions directory exists.
402
+ */
403
+ async ensureSessionsDir() {
404
+ await fs.mkdir(this.sessionsDir, { recursive: true });
405
+ }
406
+ }
@@ -92,7 +92,7 @@ export class ContextTreeStructureContributor {
92
92
  if (truncatedCount.value > 0) {
93
93
  parts.push('', `[${truncatedCount.value} additional entries not shown]`);
94
94
  }
95
- parts.push('', '## Structure Guide', '- Each top-level folder is a **domain** (e.g., `code_style/`, `design/`, `structure/`)', '- Inside domains are **topics** as `.md` files or subfolders with `context.md`', '- `context.md` files contain the curated knowledge content', '', '## Usage', '- **Query commands**: Search ONLY within this context tree structure', '- **Curate commands**: Check existing content here before creating new topics', '</context-tree-structure>');
95
+ parts.push('', '## Structure Guide', '- Each top-level folder is a **domain** (dynamically created based on content)', '- Inside domains are **topics** as `.md` files or subfolders with `context.md`', '- `context.md` files contain the curated knowledge content', '', '## Dynamic Domains', '- Domains are created dynamically based on the semantics of curated content', '- Domain names should be descriptive, use snake_case (e.g., `authentication`, `api_design`)', '- Before creating a new domain, check if existing domains could accommodate the content', '', '## Usage', '- **Query commands**: Search ONLY within this context tree structure', '- **Curate commands**: Check existing domains/topics before creating new ones', '</context-tree-structure>');
96
96
  return parts.join('\n');
97
97
  }
98
98
  /**
@@ -105,7 +105,9 @@ export class ContextTreeStructureContributor {
105
105
  '',
106
106
  'The context tree at `.brv/context-tree/` exists but contains no curated content yet.',
107
107
  '',
108
- '**For curate commands**: You can create new knowledge topics in any domain.',
108
+ '**For curate commands**: Create new domains and topics dynamically based on content.',
109
+ '- Choose semantically meaningful domain names (e.g., `authentication`, `api_design`, `data_models`)',
110
+ '- Use snake_case format for domain names',
109
111
  '**For query commands**: No context is available to search.',
110
112
  '</context-tree-structure>',
111
113
  ].join('\n');
@@ -7,16 +7,16 @@ import { sanitizeFolderName } from '../../../../utils/file-helpers.js';
7
7
  const CreateKnowledgeTopicInputSchema = z.object({
8
8
  // Base path for knowledge storage
9
9
  basePath: z.string().default('.brv/context-tree'),
10
- domains: z.array(z.string()).describe('Array of domain names'),
10
+ domains: z.array(z.string()).describe('Array of domain names (dynamically created based on content)'),
11
11
  // Manual topics (optional)
12
12
  topics: z
13
13
  .array(z.object({
14
- domain: z.string().describe('Domain category name from predefined list'),
14
+ domain: z.string().describe('Domain category name (can be any semantically meaningful name)'),
15
15
  name: z.string().describe('Topic name'),
16
16
  relations: z
17
17
  .array(z.string())
18
18
  .optional()
19
- .describe('Related topics using @domain/topic or @domain/topic/subtopic notation'),
19
+ .describe('Related topics using domain/topic or domain/topic/subtopic notation'),
20
20
  snippets: z.array(z.string()).describe('Code/text snippets'),
21
21
  subtopics: z
22
22
  .array(z.object({
@@ -24,7 +24,7 @@ const CreateKnowledgeTopicInputSchema = z.object({
24
24
  relations: z
25
25
  .array(z.string())
26
26
  .optional()
27
- .describe('Related topics using @domain/topic or @domain/topic/subtopic notation'),
27
+ .describe('Related topics using domain/topic or domain/topic/subtopic notation'),
28
28
  snippets: z.array(z.string()).describe('Code/text snippets'),
29
29
  }))
30
30
  .describe('Array of subtopics'),
@@ -112,27 +112,34 @@ async function executeCreateKnowledgeTopic(input, _context) {
112
112
  */
113
113
  export function createCreateKnowledgeTopicTool() {
114
114
  return {
115
- description: `Create organized knowledge topics within domain folders. This tool structures knowledge by creating topic and subtopic folders, each containing a context.md file with relevant snippets and optional relations.
115
+ description: `Create organized knowledge topics within dynamically-created domain folders. This tool structures knowledge by creating domain, topic, and subtopic folders, each containing a context.md file with relevant snippets and optional relations.
116
116
 
117
- Use this tool after detecting domains to organize extracted knowledge into a hierarchical structure:
118
- - Domain folders (e.g., .brv/context-tree/domain-name/)
119
- - Topic folders (e.g., .brv/context-tree/domain-name/topic-name/)
120
- - Topic context.md files (e.g., .brv/context-tree/domain-name/topic-name/context.md)
121
- - Subtopic folders (e.g., .brv/context-tree/domain-name/topic-name/subtopic-name/)
122
- - Subtopic context.md files (e.g., .brv/context-tree/domain-name/topic-name/subtopic-name/context.md)
117
+ **Dynamic Domain Creation:**
118
+ Domains are created dynamically based on the content being organized. Choose domain names that:
119
+ - Are semantically meaningful and descriptive (e.g., "authentication", "api_design", "data_models")
120
+ - Use snake_case format (1-3 words)
121
+ - Group related concepts together
122
+ - Avoid overly generic names (e.g., "misc", "other") or overly specific names
123
123
 
124
- Each topic should include:
124
+ **Hierarchical Structure:**
125
+ - Domain folders (e.g., .brv/context-tree/authentication/)
126
+ - Topic folders (e.g., .brv/context-tree/authentication/oauth_flow/)
127
+ - Topic context.md files (e.g., .brv/context-tree/authentication/oauth_flow/context.md)
128
+ - Subtopic folders (e.g., .brv/context-tree/authentication/oauth_flow/token_refresh/)
129
+ - Subtopic context.md files (e.g., .brv/context-tree/authentication/oauth_flow/token_refresh/context.md)
130
+
131
+ **Each topic should include:**
125
132
  1. A clear topic name
126
133
  2. Relevant code/text snippets that demonstrate the knowledge
127
- 3. Optional relations to other topics using @domain/topic or @domain/topic/subtopic notation
134
+ 3. Optional relations to other topics using domain/topic or domain/topic/subtopic notation
128
135
  4. Subtopics (optional) that break down the topic into smaller pieces
129
136
 
130
- Relations enhance knowledge discovery by linking related contexts. Example:
131
- - relations: ['code_style/error-handling', 'structure/api-endpoints/validation']
137
+ **Relations** enhance knowledge discovery by linking related contexts. Example:
138
+ - relations: ['authentication/session_management', 'api_design/endpoints/validation']
132
139
 
133
- The tool automatically:
140
+ **The tool automatically:**
134
141
  - Creates the base knowledge structure if it doesn't exist
135
- - Creates topic and subtopic folders as needed
142
+ - Creates domain, topic, and subtopic folders as needed
136
143
  - Generates context.md files with snippets and relations
137
144
  - Handles existing topics gracefully (updates instead of recreating)`,
138
145
  execute: executeCreateKnowledgeTopic,