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.
- package/README.md +75 -12
- package/dist/commands/curate.js +3 -3
- package/dist/commands/main.d.ts +13 -0
- package/dist/commands/main.js +55 -4
- package/dist/commands/query.js +3 -3
- package/dist/commands/status.js +2 -2
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +4 -1
- package/dist/core/domain/cipher/file-system/types.d.ts +2 -0
- package/dist/core/domain/cipher/llm/registry.js +53 -2
- package/dist/core/domain/cipher/llm/types.d.ts +2 -0
- package/dist/core/domain/cipher/process/types.d.ts +7 -0
- package/dist/core/domain/cipher/session/session-metadata.d.ts +178 -0
- package/dist/core/domain/cipher/session/session-metadata.js +147 -0
- package/dist/core/domain/entities/auth-token.js +6 -3
- package/dist/core/domain/entities/event.d.ts +1 -1
- package/dist/core/domain/entities/event.js +2 -1
- package/dist/core/domain/knowledge/markdown-writer.d.ts +15 -18
- package/dist/core/domain/knowledge/markdown-writer.js +232 -34
- package/dist/core/domain/knowledge/relation-parser.d.ts +37 -36
- package/dist/core/domain/knowledge/relation-parser.js +53 -58
- package/dist/core/domain/transport/schemas.d.ts +52 -1
- package/dist/core/domain/transport/schemas.js +30 -1
- package/dist/core/interfaces/cipher/i-blob-storage.d.ts +6 -0
- package/dist/core/interfaces/cipher/i-session-persistence.d.ts +133 -0
- package/dist/core/interfaces/cipher/i-session-persistence.js +7 -0
- package/dist/core/interfaces/cipher/index.d.ts +0 -1
- package/dist/core/interfaces/cipher/message-types.d.ts +6 -0
- package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -0
- package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
- package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -1
- package/dist/infra/cipher/agent/agent-schemas.d.ts +6 -6
- package/dist/infra/cipher/agent/cipher-agent.js +4 -0
- package/dist/infra/cipher/agent/service-initializer.js +4 -4
- package/dist/infra/cipher/file-system/context-tree-file-system-factory.js +3 -2
- package/dist/infra/cipher/file-system/file-system-service.d.ts +4 -0
- package/dist/infra/cipher/file-system/file-system-service.js +6 -0
- package/dist/infra/cipher/http/internal-llm-http-service.js +3 -5
- package/dist/infra/cipher/interactive-loop.js +3 -1
- package/dist/infra/cipher/llm/context/context-manager.js +40 -16
- package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +98 -6
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -2
- package/dist/infra/cipher/llm/thought-parser.d.ts +21 -0
- package/dist/infra/cipher/llm/thought-parser.js +27 -0
- package/dist/infra/cipher/llm/tool-output-processor.d.ts +10 -0
- package/dist/infra/cipher/llm/tool-output-processor.js +80 -7
- package/dist/infra/cipher/process/process-service.js +11 -3
- package/dist/infra/cipher/session/chat-session.d.ts +7 -2
- package/dist/infra/cipher/session/chat-session.js +90 -52
- package/dist/infra/cipher/session/session-metadata-store.d.ts +52 -0
- package/dist/infra/cipher/session/session-metadata-store.js +406 -0
- package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +4 -2
- package/dist/infra/cipher/tools/implementations/create-knowledge-topic-tool.js +24 -17
- package/dist/infra/cipher/tools/implementations/curate-tool.js +138 -65
- package/dist/infra/cipher/tools/implementations/read-file-tool.js +3 -12
- package/dist/infra/cipher/tools/implementations/spec-analyze-tool.js +18 -15
- package/dist/infra/cipher/tools/implementations/task-tool.js +54 -7
- package/dist/infra/context-tree/file-context-file-reader.js +4 -0
- package/dist/infra/context-tree/file-context-tree-service.js +4 -15
- package/dist/infra/core/executors/curate-executor.d.ts +2 -7
- package/dist/infra/core/executors/curate-executor.js +18 -53
- package/dist/infra/core/executors/query-executor.d.ts +1 -7
- package/dist/infra/core/executors/query-executor.js +10 -35
- package/dist/infra/core/task-processor.d.ts +2 -0
- package/dist/infra/core/task-processor.js +1 -0
- package/dist/infra/http/authenticated-http-client.js +5 -0
- package/dist/infra/process/agent-worker.js +113 -6
- package/dist/infra/process/constants.d.ts +1 -0
- package/dist/infra/process/constants.js +1 -0
- package/dist/infra/process/process-manager.d.ts +10 -1
- package/dist/infra/process/process-manager.js +16 -6
- package/dist/infra/process/task-queue-manager.js +2 -1
- package/dist/infra/process/transport-handlers.js +35 -0
- package/dist/infra/process/transport-worker.js +89 -1
- package/dist/infra/repl/commands/curate-command.js +2 -2
- package/dist/infra/repl/commands/gen-rules-command.js +2 -2
- package/dist/infra/repl/commands/index.js +5 -2
- package/dist/infra/repl/commands/init-command.js +2 -2
- package/dist/infra/repl/commands/login-command.js +2 -2
- package/dist/infra/repl/commands/logout-command.js +2 -2
- package/dist/infra/repl/commands/new-command.d.ts +14 -0
- package/dist/infra/repl/commands/new-command.js +61 -0
- package/dist/infra/repl/commands/pull-command.js +2 -2
- package/dist/infra/repl/commands/push-command.js +2 -2
- package/dist/infra/repl/commands/query-command.js +2 -2
- package/dist/infra/repl/commands/{clear-command.d.ts → reset-command.d.ts} +2 -2
- package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +10 -10
- package/dist/infra/repl/commands/space/list-command.js +2 -2
- package/dist/infra/repl/commands/space/switch-command.js +2 -2
- package/dist/infra/repl/commands/status-command.js +2 -2
- package/dist/infra/repl/repl-startup.js +0 -2
- package/dist/infra/storage/file-token-store.d.ts +31 -0
- package/dist/infra/storage/file-token-store.js +98 -0
- package/dist/infra/storage/keychain-token-store.d.ts +4 -1
- package/dist/infra/storage/keychain-token-store.js +6 -4
- package/dist/infra/storage/token-store.d.ts +10 -0
- package/dist/infra/storage/token-store.js +14 -0
- package/dist/infra/usecase/curate-use-case.js +1 -1
- package/dist/infra/usecase/generate-rules-use-case.js +2 -2
- package/dist/infra/usecase/init-use-case.js +4 -4
- package/dist/infra/usecase/logout-use-case.js +1 -1
- package/dist/infra/usecase/push-use-case.js +1 -1
- package/dist/infra/usecase/{clear-use-case.d.ts → reset-use-case.d.ts} +5 -5
- package/dist/infra/usecase/{clear-use-case.js → reset-use-case.js} +5 -5
- package/dist/infra/user/http-user-service.js +6 -11
- package/dist/resources/prompts/curate.yml +79 -15
- package/dist/resources/prompts/plan.yml +6 -0
- package/dist/resources/tools/curate.txt +60 -15
- package/dist/tui/app.js +1 -1
- package/dist/tui/components/execution/log-item.js +2 -5
- package/dist/tui/components/header.d.ts +1 -1
- package/dist/tui/components/header.js +25 -4
- package/dist/tui/components/index.d.ts +5 -1
- package/dist/tui/components/index.js +3 -1
- package/dist/tui/components/init.d.ts +33 -0
- package/dist/tui/components/init.js +253 -0
- package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
- package/dist/tui/components/onboarding/index.d.ts +1 -0
- package/dist/tui/components/onboarding/index.js +1 -0
- package/dist/tui/components/onboarding/onboarding-flow.d.ts +2 -0
- package/dist/tui/components/onboarding/onboarding-flow.js +9 -229
- package/dist/tui/components/onboarding/onboarding-step.js +1 -1
- package/dist/tui/components/onboarding/welcome-box.d.ts +14 -0
- package/dist/tui/components/onboarding/welcome-box.js +23 -0
- package/dist/tui/components/status-badge.d.ts +22 -0
- package/dist/tui/components/status-badge.js +32 -0
- package/dist/tui/contexts/auth-context.js +2 -1
- package/dist/tui/contexts/index.d.ts +1 -0
- package/dist/tui/contexts/index.js +1 -0
- package/dist/tui/contexts/onboarding-context.d.ts +14 -0
- package/dist/tui/contexts/onboarding-context.js +17 -22
- package/dist/tui/contexts/status-context.d.ts +33 -0
- package/dist/tui/contexts/status-context.js +159 -0
- package/dist/tui/hooks/use-auth-polling.d.ts +4 -1
- package/dist/tui/hooks/use-auth-polling.js +21 -7
- package/dist/tui/hooks/use-tab-navigation.js +0 -2
- package/dist/tui/providers/app-providers.js +2 -2
- package/dist/tui/types/index.d.ts +2 -0
- package/dist/tui/types/index.js +2 -0
- package/dist/tui/types/status.d.ts +46 -0
- package/dist/tui/types/status.js +13 -0
- package/dist/tui/utils/index.d.ts +6 -0
- package/dist/tui/utils/index.js +6 -0
- package/dist/tui/utils/time.d.ts +10 -0
- package/dist/tui/utils/time.js +15 -0
- package/dist/tui/views/command-view.js +15 -2
- package/dist/tui/views/index.d.ts +1 -0
- package/dist/tui/views/index.js +1 -0
- package/dist/tui/views/init-view.d.ts +15 -0
- package/dist/tui/views/init-view.js +29 -0
- package/dist/tui/views/logs-view.js +22 -8
- package/dist/utils/environment-detector.d.ts +5 -0
- package/dist/utils/environment-detector.js +31 -0
- package/dist/utils/file-validator.js +9 -7
- package/dist/utils/global-data-path.d.ts +11 -0
- package/dist/utils/global-data-path.js +32 -0
- package/oclif.manifest.json +3 -3
- package/package.json +1 -1
- package/dist/config/context-tree-domains.d.ts +0 -17
- package/dist/config/context-tree-domains.js +0 -34
- package/dist/core/interfaces/cipher/i-agent-storage.d.ts +0 -152
- package/dist/core/interfaces/usecase/i-clear-use-case.js +0 -1
- package/dist/infra/cipher/consumer/consumer-lock.d.ts +0 -20
- package/dist/infra/cipher/consumer/consumer-lock.js +0 -41
- package/dist/infra/cipher/consumer/consumer-service.d.ts +0 -99
- package/dist/infra/cipher/consumer/consumer-service.js +0 -166
- package/dist/infra/cipher/consumer/execution-consumer.d.ts +0 -126
- package/dist/infra/cipher/consumer/execution-consumer.js +0 -561
- package/dist/infra/cipher/consumer/index.d.ts +0 -33
- package/dist/infra/cipher/consumer/index.js +0 -34
- package/dist/infra/cipher/consumer/queue-polling-service.d.ts +0 -120
- package/dist/infra/cipher/consumer/queue-polling-service.js +0 -249
- package/dist/infra/cipher/storage/agent-storage.d.ts +0 -246
- package/dist/infra/cipher/storage/agent-storage.js +0 -956
- /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** (
|
|
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**:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
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
|
-
|
|
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
|
|
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: ['
|
|
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,
|