byterover-cli 1.0.4 → 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 (72) hide show
  1. package/README.md +13 -2
  2. package/dist/commands/curate.js +1 -1
  3. package/dist/commands/main.d.ts +13 -0
  4. package/dist/commands/main.js +53 -2
  5. package/dist/commands/query.js +1 -1
  6. package/dist/constants.d.ts +1 -1
  7. package/dist/constants.js +1 -1
  8. package/dist/core/domain/cipher/llm/registry.js +53 -2
  9. package/dist/core/domain/cipher/llm/types.d.ts +2 -0
  10. package/dist/core/domain/cipher/process/types.d.ts +7 -0
  11. package/dist/core/domain/cipher/session/session-metadata.d.ts +178 -0
  12. package/dist/core/domain/cipher/session/session-metadata.js +147 -0
  13. package/dist/core/domain/knowledge/markdown-writer.d.ts +15 -18
  14. package/dist/core/domain/knowledge/markdown-writer.js +232 -34
  15. package/dist/core/domain/knowledge/relation-parser.d.ts +25 -39
  16. package/dist/core/domain/knowledge/relation-parser.js +39 -61
  17. package/dist/core/domain/transport/schemas.d.ts +37 -2
  18. package/dist/core/domain/transport/schemas.js +23 -2
  19. package/dist/core/interfaces/cipher/i-session-persistence.d.ts +133 -0
  20. package/dist/core/interfaces/cipher/i-session-persistence.js +7 -0
  21. package/dist/core/interfaces/cipher/message-types.d.ts +6 -0
  22. package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -2
  23. package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
  24. package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -1
  25. package/dist/infra/cipher/agent/agent-schemas.d.ts +6 -6
  26. package/dist/infra/cipher/agent/service-initializer.js +4 -4
  27. package/dist/infra/cipher/file-system/context-tree-file-system-factory.js +3 -2
  28. package/dist/infra/cipher/file-system/file-system-service.js +1 -0
  29. package/dist/infra/cipher/http/internal-llm-http-service.js +3 -5
  30. package/dist/infra/cipher/interactive-loop.js +3 -1
  31. package/dist/infra/cipher/llm/context/context-manager.js +40 -16
  32. package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
  33. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +98 -6
  34. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -2
  35. package/dist/infra/cipher/llm/thought-parser.d.ts +21 -0
  36. package/dist/infra/cipher/llm/thought-parser.js +27 -0
  37. package/dist/infra/cipher/llm/tool-output-processor.d.ts +10 -0
  38. package/dist/infra/cipher/llm/tool-output-processor.js +80 -7
  39. package/dist/infra/cipher/process/process-service.js +11 -3
  40. package/dist/infra/cipher/session/chat-session.d.ts +7 -2
  41. package/dist/infra/cipher/session/chat-session.js +90 -52
  42. package/dist/infra/cipher/session/session-metadata-store.d.ts +52 -0
  43. package/dist/infra/cipher/session/session-metadata-store.js +406 -0
  44. package/dist/infra/cipher/tools/implementations/curate-tool.js +113 -35
  45. package/dist/infra/cipher/tools/implementations/task-tool.js +1 -0
  46. package/dist/infra/context-tree/file-context-file-reader.js +4 -0
  47. package/dist/infra/core/task-processor.d.ts +2 -2
  48. package/dist/infra/process/process-manager.d.ts +10 -1
  49. package/dist/infra/process/process-manager.js +16 -6
  50. package/dist/infra/process/transport-handlers.js +31 -0
  51. package/dist/infra/repl/commands/index.js +5 -2
  52. package/dist/infra/repl/commands/new-command.d.ts +14 -0
  53. package/dist/infra/repl/commands/new-command.js +61 -0
  54. package/dist/infra/repl/commands/{clear-command.d.ts → reset-command.d.ts} +2 -2
  55. package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +10 -10
  56. package/dist/infra/usecase/generate-rules-use-case.js +2 -2
  57. package/dist/infra/usecase/init-use-case.js +4 -4
  58. package/dist/infra/usecase/logout-use-case.js +1 -1
  59. package/dist/infra/usecase/push-use-case.js +1 -1
  60. package/dist/infra/usecase/{clear-use-case.d.ts → reset-use-case.d.ts} +5 -5
  61. package/dist/infra/usecase/{clear-use-case.js → reset-use-case.js} +5 -5
  62. package/dist/resources/prompts/curate.yml +68 -13
  63. package/dist/resources/tools/curate.txt +60 -15
  64. package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
  65. package/dist/tui/components/onboarding/onboarding-flow.js +1 -0
  66. package/dist/tui/views/command-view.js +15 -0
  67. package/dist/utils/file-validator.js +9 -7
  68. package/oclif.manifest.json +3 -3
  69. package/package.json +1 -1
  70. package/dist/config/context-tree-domains.d.ts +0 -29
  71. package/dist/config/context-tree-domains.js +0 -29
  72. /package/dist/core/interfaces/usecase/{i-clear-use-case.js → i-reset-use-case.js} +0 -0
package/README.md CHANGED
@@ -203,11 +203,22 @@ This creates agent-specific rule files (e.g., `CLAUDE.md`, `.cursorrules`) that
203
203
  | Command | Description |
204
204
  |---------|-------------|
205
205
  | `/gen-rules` | Generate rule files for AI coding agents (Claude Code, Cursor, etc.) |
206
- | `/clear [-y] [directory]` | Reset context tree to default domains |
206
+ | `/reset [-y] [directory]` | Reset context tree to default domains |
207
207
 
208
- **Clear options:**
208
+ **Reset options:**
209
209
  - `-y, --yes`: Skip confirmation prompt
210
210
 
211
+ ### Session Management
212
+
213
+ | Command | Description |
214
+ |---------|-------------|
215
+ | `/new [-y]` | Start a fresh session (ends current session, clears conversation) |
216
+
217
+ **Options:**
218
+ - `-y, --yes`: Skip confirmation prompt
219
+
220
+ **Note:** This command does NOT affect the context tree—it only clears the conversation history and starts a new session.
221
+
211
222
  ### Project Setup
212
223
 
213
224
  | Command | Description |
@@ -14,7 +14,7 @@ export default class Curate extends Command {
14
14
  };
15
15
  static description = `Curate context to the context tree (connects to running brv instance)
16
16
 
17
- Requires a running brv instance. Start one with: brv start
17
+ Requires a running brv instance. Start one with: brv
18
18
 
19
19
  Good examples:
20
20
  - "Auth uses JWT with 24h expiry. Tokens stored in httpOnly cookies via authMiddleware.ts"
@@ -14,4 +14,17 @@ export default class Main extends Command {
14
14
  */
15
15
  static hidden: boolean;
16
16
  run(): Promise<void>;
17
+ /**
18
+ * Resolve session ID for the agent.
19
+ *
20
+ * Strategy:
21
+ * 1. Check for active session in .brv/sessions/active.json
22
+ * 2. If active session exists and is valid (not stale), resume it
23
+ * 3. If stale (process crashed), mark as interrupted and create new
24
+ * 4. If no active session, create new session
25
+ * 5. Run session cleanup on startup
26
+ *
27
+ * @returns Session ID to use
28
+ */
29
+ private resolveSessionId;
17
30
  }
@@ -1,4 +1,7 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { DEFAULT_SESSION_RETENTION } from '../core/domain/cipher/session/session-metadata.js';
4
+ import { SessionMetadataStore } from '../infra/cipher/session/session-metadata-store.js';
2
5
  import { ProjectConfigStore } from '../infra/config/file-config-store.js';
3
6
  import { getProcessManager } from '../infra/process/index.js';
4
7
  import { startRepl } from '../infra/repl/repl-startup.js';
@@ -6,7 +9,7 @@ import { FileGlobalConfigStore } from '../infra/storage/file-global-config-store
6
9
  import { FileOnboardingPreferenceStore } from '../infra/storage/file-onboarding-preference-store.js';
7
10
  import { createTokenStore } from '../infra/storage/token-store.js';
8
11
  import { MixpanelTrackingService } from '../infra/tracking/mixpanel-tracking-service.js';
9
- import { initSessionLog } from '../utils/process-logger.js';
12
+ import { initSessionLog, processManagerLog } from '../utils/process-logger.js';
10
13
  /**
11
14
  * Main command - Entry point for ByteRover CLI.
12
15
  *
@@ -30,9 +33,13 @@ export default class Main extends Command {
30
33
  this.log("Run 'brv --help' for available commands.");
31
34
  return;
32
35
  }
36
+ // Resolve session ID (auto-resume or create new)
37
+ const sessionId = await this.resolveSessionId();
38
+ processManagerLog(`Session ID resolved: ${sessionId}`);
33
39
  // Start Transport and Agent processes (v0.5.0 architecture)
40
+ // Pass session ID to Agent via environment variable
34
41
  const processManager = getProcessManager();
35
- await processManager.start();
42
+ await processManager.start({ sessionId });
36
43
  const tokenStore = createTokenStore();
37
44
  const globalConfigStore = new FileGlobalConfigStore();
38
45
  const trackingService = new MixpanelTrackingService({ globalConfigStore, tokenStore });
@@ -52,4 +59,48 @@ export default class Main extends Command {
52
59
  await processManager.stop();
53
60
  }
54
61
  }
62
+ /**
63
+ * Resolve session ID for the agent.
64
+ *
65
+ * Strategy:
66
+ * 1. Check for active session in .brv/sessions/active.json
67
+ * 2. If active session exists and is valid (not stale), resume it
68
+ * 3. If stale (process crashed), mark as interrupted and create new
69
+ * 4. If no active session, create new session
70
+ * 5. Run session cleanup on startup
71
+ *
72
+ * @returns Session ID to use
73
+ */
74
+ async resolveSessionId() {
75
+ const sessionStore = new SessionMetadataStore();
76
+ // Run cleanup on startup (async, don't wait)
77
+ sessionStore.cleanupSessions(DEFAULT_SESSION_RETENTION).catch((error) => {
78
+ processManagerLog(`Session cleanup failed: ${error}`);
79
+ });
80
+ // Check for active session
81
+ const activeSession = await sessionStore.getActiveSession();
82
+ if (activeSession) {
83
+ // Check if the active session is stale (process not running)
84
+ const isStale = await sessionStore.isActiveSessionStale();
85
+ if (isStale) {
86
+ // Mark the old session as interrupted
87
+ processManagerLog(`Active session ${activeSession.sessionId} is stale, marking as interrupted`);
88
+ await sessionStore.markSessionInterrupted(activeSession.sessionId);
89
+ }
90
+ else {
91
+ // Valid active session - resume it
92
+ processManagerLog(`Resuming active session: ${activeSession.sessionId}`);
93
+ return activeSession.sessionId;
94
+ }
95
+ }
96
+ // Create new session
97
+ const newSessionId = `agent-session-${randomUUID()}`;
98
+ processManagerLog(`Creating new session: ${newSessionId}`);
99
+ // Save session metadata
100
+ const metadata = sessionStore.createSessionMetadata(newSessionId);
101
+ await sessionStore.saveSession(metadata);
102
+ // Set as active session
103
+ await sessionStore.setActiveSession(newSessionId);
104
+ return newSessionId;
105
+ }
55
106
  }
@@ -14,7 +14,7 @@ export default class Query extends Command {
14
14
  };
15
15
  static description = `Query and retrieve information from the context tree (connects to running brv instance)
16
16
 
17
- Requires a running brv instance. Start one with: brv start
17
+ Requires a running brv instance. Start one with: brv
18
18
 
19
19
  Good:
20
20
  - "How is user authentication implemented?"
@@ -34,4 +34,4 @@ export declare const TRANSPORT_RECONNECTION_ATTEMPTS = 30;
34
34
  export declare const TRANSPORT_PING_INTERVAL_MS = 5000;
35
35
  export declare const TRANSPORT_PING_TIMEOUT_MS = 10000;
36
36
  export declare const TRANSPORT_DEFAULT_TRANSPORTS: ('polling' | 'websocket')[];
37
- export declare const DEFAULT_LLM_MODEL = "gemini-2.5-pro";
37
+ export declare const DEFAULT_LLM_MODEL = "gemini-3-flash-preview";
package/dist/constants.js CHANGED
@@ -43,4 +43,4 @@ export const TRANSPORT_PING_TIMEOUT_MS = 10_000; // 10s timeout - avoid false di
43
43
  // HTTP polling may be blocked by IDE sandboxes causing "xhr poll error"
44
44
  export const TRANSPORT_DEFAULT_TRANSPORTS = ['websocket'];
45
45
  // LLM Model defaults
46
- export const DEFAULT_LLM_MODEL = 'gemini-2.5-pro';
46
+ export const DEFAULT_LLM_MODEL = 'gemini-3-flash-preview';
@@ -27,19 +27,54 @@ export const LLM_REGISTRY = {
27
27
  supportedFileTypes: [],
28
28
  },
29
29
  gemini: {
30
- defaultModel: 'gemini-2.5-flash',
30
+ defaultModel: 'gemini-3-flash-preview',
31
31
  models: [
32
- // Gemini 2.5 series
32
+ // Gemini 3 series (Preview)
33
33
  {
34
34
  capabilities: {
35
35
  supportsAudio: true,
36
36
  supportsImages: true,
37
+ supportsMultimodalFunctionResponse: true,
37
38
  supportsPdf: true,
38
39
  supportsStreaming: true,
39
40
  supportsThinking: true,
40
41
  },
41
42
  charsPerToken: 4,
42
43
  default: true,
44
+ displayName: 'Gemini 3 Flash (Preview)',
45
+ maxInputTokens: 1_000_000,
46
+ maxOutputTokens: 8192,
47
+ name: 'gemini-3-flash-preview',
48
+ pricing: { inputPerM: 0.075, outputPerM: 0.3 },
49
+ supportedFileTypes: ['image', 'pdf', 'audio'],
50
+ },
51
+ {
52
+ capabilities: {
53
+ supportsAudio: true,
54
+ supportsImages: true,
55
+ supportsMultimodalFunctionResponse: true,
56
+ supportsPdf: true,
57
+ supportsStreaming: true,
58
+ supportsThinking: true,
59
+ },
60
+ charsPerToken: 4,
61
+ displayName: 'Gemini 3 Pro (Preview)',
62
+ maxInputTokens: 1_000_000,
63
+ maxOutputTokens: 8192,
64
+ name: 'gemini-3-pro-preview',
65
+ pricing: { inputPerM: 1.25, outputPerM: 5 },
66
+ supportedFileTypes: ['image', 'pdf', 'audio'],
67
+ },
68
+ // Gemini 2.5 series
69
+ {
70
+ capabilities: {
71
+ supportsAudio: true,
72
+ supportsImages: true,
73
+ supportsPdf: true,
74
+ supportsStreaming: true,
75
+ supportsThinking: true,
76
+ },
77
+ charsPerToken: 4,
43
78
  displayName: 'Gemini 2.5 Flash',
44
79
  maxInputTokens: 1_000_000,
45
80
  maxOutputTokens: 8192,
@@ -47,6 +82,22 @@ export const LLM_REGISTRY = {
47
82
  pricing: { inputPerM: 0.075, outputPerM: 0.3 },
48
83
  supportedFileTypes: ['image', 'pdf', 'audio'],
49
84
  },
85
+ {
86
+ capabilities: {
87
+ supportsAudio: true,
88
+ supportsImages: true,
89
+ supportsPdf: true,
90
+ supportsStreaming: true,
91
+ supportsThinking: true,
92
+ },
93
+ charsPerToken: 4,
94
+ displayName: 'Gemini 2.5 Pro',
95
+ maxInputTokens: 1_000_000,
96
+ maxOutputTokens: 8192,
97
+ name: 'gemini-2.5-pro',
98
+ pricing: { inputPerM: 1.25, outputPerM: 5 },
99
+ supportedFileTypes: ['image', 'pdf', 'audio'],
100
+ },
50
101
  // Gemini 1.5 series
51
102
  {
52
103
  capabilities: {
@@ -28,6 +28,8 @@ export interface ModelCapabilities {
28
28
  supportsAudio: boolean;
29
29
  /** Whether the model supports image input */
30
30
  supportsImages: boolean;
31
+ /** Whether the model supports multimodal data in function responses (Gemini 3+) */
32
+ supportsMultimodalFunctionResponse?: boolean;
31
33
  /** Whether the model supports PDF input */
32
34
  supportsPdf: boolean;
33
35
  /** Whether the model supports streaming responses */
@@ -17,6 +17,13 @@ export interface ProcessConfig {
17
17
  * Custom environment variables for all commands.
18
18
  */
19
19
  environment: Record<string, string>;
20
+ /**
21
+ * Grace period in milliseconds to wait after SIGTERM before sending SIGKILL.
22
+ * Allows processes time to clean up gracefully.
23
+ *
24
+ * @default 5000 (5 seconds)
25
+ */
26
+ killGracePeriod: number;
20
27
  /**
21
28
  * Maximum number of concurrent background processes.
22
29
  * @default 5
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Session Metadata Types and Schemas
3
+ *
4
+ * Defines the data structures for persistent session management.
5
+ * Sessions are stored in .brv/sessions/ directory as JSON files.
6
+ *
7
+ * Design adapted from gemini-cli's ChatRecordingService pattern.
8
+ */
9
+ import { z } from 'zod';
10
+ /**
11
+ * Session status indicating lifecycle state.
12
+ */
13
+ export type SessionStatus = 'active' | 'ended' | 'interrupted';
14
+ /**
15
+ * Active session pointer stored in .brv/sessions/active.json
16
+ * Points to the currently active session for auto-resume.
17
+ */
18
+ export interface ActiveSessionPointer {
19
+ /** ISO timestamp when this session became active */
20
+ activatedAt: string;
21
+ /** PID of the process that activated this session (for stale detection) */
22
+ pid: number;
23
+ /**
24
+ * Unique token identifying this specific process instance.
25
+ * Used to detect PID reuse: if the PID exists but token differs,
26
+ * it's a different process that got the same PID after the original crashed.
27
+ * Optional for backward compatibility with existing session files.
28
+ */
29
+ processToken?: string;
30
+ /** Session ID of the currently active session */
31
+ sessionId: string;
32
+ }
33
+ /**
34
+ * Session metadata stored in .brv/sessions/session-*.json
35
+ * Contains metadata about a session for listing and management.
36
+ */
37
+ export interface SessionMetadata {
38
+ /** ISO timestamp when session was created */
39
+ createdAt: string;
40
+ /** ISO timestamp of last activity */
41
+ lastUpdated: string;
42
+ /** Number of messages in session (cached for quick display) */
43
+ messageCount: number;
44
+ /** Unique session identifier (UUID) */
45
+ sessionId: string;
46
+ /** Session lifecycle status */
47
+ status: SessionStatus;
48
+ /** Optional AI-generated summary */
49
+ summary?: string;
50
+ /** Session title (generated from first user message) */
51
+ title?: string;
52
+ /** Project working directory (for validation) */
53
+ workingDirectory: string;
54
+ }
55
+ /**
56
+ * Session info for display purposes (extends metadata with computed fields).
57
+ */
58
+ export interface SessionInfo extends SessionMetadata {
59
+ /** Filename without extension */
60
+ file: string;
61
+ /** Full filename including .json extension */
62
+ fileName: string;
63
+ /** First user message content (cleaned) */
64
+ firstUserMessage?: string;
65
+ /** Display index in the list (1-based) */
66
+ index: number;
67
+ /** Whether this is the currently active session */
68
+ isCurrentSession: boolean;
69
+ }
70
+ /**
71
+ * Result of resolving a session selection.
72
+ */
73
+ export interface SessionSelectionResult {
74
+ /** Display info string for user feedback */
75
+ displayInfo: string;
76
+ /** Loaded session metadata */
77
+ sessionData: SessionMetadata;
78
+ /** Full path to session metadata file */
79
+ sessionPath: string;
80
+ }
81
+ /**
82
+ * Schema for ActiveSessionPointer validation.
83
+ */
84
+ export declare const ActiveSessionPointerSchema: z.ZodObject<{
85
+ activatedAt: z.ZodUnion<[z.ZodString, z.ZodString]>;
86
+ pid: z.ZodNumber;
87
+ processToken: z.ZodOptional<z.ZodString>;
88
+ sessionId: z.ZodString;
89
+ }, "strip", z.ZodTypeAny, {
90
+ sessionId: string;
91
+ pid: number;
92
+ activatedAt: string;
93
+ processToken?: string | undefined;
94
+ }, {
95
+ sessionId: string;
96
+ pid: number;
97
+ activatedAt: string;
98
+ processToken?: string | undefined;
99
+ }>;
100
+ /**
101
+ * Schema for SessionMetadata validation.
102
+ */
103
+ export declare const SessionMetadataSchema: z.ZodObject<{
104
+ createdAt: z.ZodUnion<[z.ZodString, z.ZodString]>;
105
+ lastUpdated: z.ZodUnion<[z.ZodString, z.ZodString]>;
106
+ messageCount: z.ZodNumber;
107
+ sessionId: z.ZodString;
108
+ status: z.ZodEnum<["active", "ended", "interrupted"]>;
109
+ summary: z.ZodOptional<z.ZodString>;
110
+ title: z.ZodOptional<z.ZodString>;
111
+ workingDirectory: z.ZodString;
112
+ }, "strip", z.ZodTypeAny, {
113
+ status: "active" | "ended" | "interrupted";
114
+ sessionId: string;
115
+ createdAt: string;
116
+ lastUpdated: string;
117
+ messageCount: number;
118
+ workingDirectory: string;
119
+ summary?: string | undefined;
120
+ title?: string | undefined;
121
+ }, {
122
+ status: "active" | "ended" | "interrupted";
123
+ sessionId: string;
124
+ createdAt: string;
125
+ lastUpdated: string;
126
+ messageCount: number;
127
+ workingDirectory: string;
128
+ summary?: string | undefined;
129
+ title?: string | undefined;
130
+ }>;
131
+ /** Prefix for session metadata files */
132
+ export declare const SESSION_FILE_PREFIX = "session-";
133
+ /** Directory name for session storage */
134
+ export declare const SESSIONS_DIR = "sessions";
135
+ /** Filename for active session pointer */
136
+ export declare const ACTIVE_SESSION_FILE = "active.json";
137
+ /** Default session retention config */
138
+ export declare const DEFAULT_SESSION_RETENTION: {
139
+ /** Maximum age in days before auto-cleanup */
140
+ maxAgeDays: number;
141
+ /** Maximum number of sessions to keep */
142
+ maxCount: number;
143
+ /** Run cleanup on startup */
144
+ runOnStartup: boolean;
145
+ };
146
+ /**
147
+ * Generate a session filename from timestamp and session ID.
148
+ *
149
+ * @param sessionId - The session UUID
150
+ * @returns Filename in format: session-YYYY-MM-DDTHH-MM-SS-<uuid-prefix>.json
151
+ */
152
+ export declare function generateSessionFilename(sessionId: string): string;
153
+ /**
154
+ * Parse a session filename to extract timestamp and UUID prefix.
155
+ *
156
+ * @param filename - The session filename
157
+ * @returns Parsed components or null if invalid format
158
+ */
159
+ export declare function parseSessionFilename(filename: string): null | {
160
+ timestamp: string;
161
+ uuidPrefix: string;
162
+ };
163
+ /**
164
+ * Format a timestamp as relative time.
165
+ *
166
+ * @param timestamp - ISO timestamp string
167
+ * @param style - 'long' (e.g., "2 hours ago") or 'short' (e.g., "2h")
168
+ * @returns Formatted relative time string
169
+ */
170
+ export declare function formatRelativeTime(timestamp: string, style?: 'long' | 'short'): string;
171
+ /**
172
+ * Clean and sanitize message content for display.
173
+ * Converts newlines to spaces, collapses whitespace, removes non-printable chars.
174
+ *
175
+ * @param message - Raw message content
176
+ * @returns Cleaned message suitable for display
177
+ */
178
+ export declare function cleanMessageForTitle(message: string): string;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Session Metadata Types and Schemas
3
+ *
4
+ * Defines the data structures for persistent session management.
5
+ * Sessions are stored in .brv/sessions/ directory as JSON files.
6
+ *
7
+ * Design adapted from gemini-cli's ChatRecordingService pattern.
8
+ */
9
+ import { z } from 'zod';
10
+ // ============================================================================
11
+ // Zod Schemas for Validation
12
+ // ============================================================================
13
+ /**
14
+ * Schema for ActiveSessionPointer validation.
15
+ */
16
+ export const ActiveSessionPointerSchema = z.object({
17
+ activatedAt: z.string().datetime({ offset: true }).or(z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)),
18
+ pid: z.number().int().positive(),
19
+ // Optional for backward compatibility with existing session files (treated as stale if missing)
20
+ processToken: z.string().optional(),
21
+ sessionId: z.string().min(1),
22
+ });
23
+ /**
24
+ * Schema for SessionMetadata validation.
25
+ */
26
+ export const SessionMetadataSchema = z.object({
27
+ createdAt: z.string().datetime({ offset: true }).or(z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)),
28
+ lastUpdated: z.string().datetime({ offset: true }).or(z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)),
29
+ messageCount: z.number().int().nonnegative(),
30
+ sessionId: z.string().min(1),
31
+ status: z.enum(['active', 'ended', 'interrupted']),
32
+ summary: z.string().optional(),
33
+ title: z.string().optional(),
34
+ workingDirectory: z.string().min(1),
35
+ });
36
+ // ============================================================================
37
+ // Constants
38
+ // ============================================================================
39
+ /** Prefix for session metadata files */
40
+ export const SESSION_FILE_PREFIX = 'session-';
41
+ /** Directory name for session storage */
42
+ export const SESSIONS_DIR = 'sessions';
43
+ /** Filename for active session pointer */
44
+ export const ACTIVE_SESSION_FILE = 'active.json';
45
+ /** Default session retention config */
46
+ export const DEFAULT_SESSION_RETENTION = {
47
+ /** Maximum age in days before auto-cleanup */
48
+ maxAgeDays: 30,
49
+ /** Maximum number of sessions to keep */
50
+ maxCount: 50,
51
+ /** Run cleanup on startup */
52
+ runOnStartup: true,
53
+ };
54
+ // ============================================================================
55
+ // Helper Functions
56
+ // ============================================================================
57
+ /**
58
+ * Generate a session filename from timestamp and session ID.
59
+ *
60
+ * @param sessionId - The session UUID
61
+ * @returns Filename in format: session-YYYY-MM-DDTHH-MM-SS-<uuid-prefix>.json
62
+ */
63
+ export function generateSessionFilename(sessionId) {
64
+ const timestamp = new Date().toISOString().slice(0, 19).replaceAll(':', '-');
65
+ const uuidPrefix = sessionId.slice(0, 8);
66
+ return `${SESSION_FILE_PREFIX}${timestamp}-${uuidPrefix}.json`;
67
+ }
68
+ /**
69
+ * Parse a session filename to extract timestamp and UUID prefix.
70
+ *
71
+ * @param filename - The session filename
72
+ * @returns Parsed components or null if invalid format
73
+ */
74
+ export function parseSessionFilename(filename) {
75
+ if (!filename.startsWith(SESSION_FILE_PREFIX) || !filename.endsWith('.json')) {
76
+ return null;
77
+ }
78
+ // Remove prefix and .json suffix
79
+ const withoutPrefix = filename.slice(SESSION_FILE_PREFIX.length, -5);
80
+ // Format: YYYY-MM-DDTHH-MM-SS-<uuid-prefix>
81
+ // The UUID prefix is the last 8 characters after the last dash
82
+ const lastDashIndex = withoutPrefix.lastIndexOf('-');
83
+ if (lastDashIndex === -1) {
84
+ return null;
85
+ }
86
+ const timestamp = withoutPrefix.slice(0, lastDashIndex);
87
+ const uuidPrefix = withoutPrefix.slice(lastDashIndex + 1);
88
+ if (uuidPrefix.length !== 8) {
89
+ return null;
90
+ }
91
+ return { timestamp, uuidPrefix };
92
+ }
93
+ /**
94
+ * Format a timestamp as relative time.
95
+ *
96
+ * @param timestamp - ISO timestamp string
97
+ * @param style - 'long' (e.g., "2 hours ago") or 'short' (e.g., "2h")
98
+ * @returns Formatted relative time string
99
+ */
100
+ export function formatRelativeTime(timestamp, style = 'long') {
101
+ const now = new Date();
102
+ const time = new Date(timestamp);
103
+ const diffMs = now.getTime() - time.getTime();
104
+ const diffSeconds = Math.floor(diffMs / 1000);
105
+ const diffMinutes = Math.floor(diffSeconds / 60);
106
+ const diffHours = Math.floor(diffMinutes / 60);
107
+ const diffDays = Math.floor(diffHours / 24);
108
+ if (style === 'short') {
109
+ if (diffSeconds < 1)
110
+ return 'now';
111
+ if (diffSeconds < 60)
112
+ return `${diffSeconds}s`;
113
+ if (diffMinutes < 60)
114
+ return `${diffMinutes}m`;
115
+ if (diffHours < 24)
116
+ return `${diffHours}h`;
117
+ if (diffDays < 30)
118
+ return `${diffDays}d`;
119
+ const diffMonths = Math.floor(diffDays / 30);
120
+ return diffMonths < 12 ? `${diffMonths}mo` : `${Math.floor(diffMonths / 12)}y`;
121
+ }
122
+ if (diffDays > 0) {
123
+ return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
124
+ }
125
+ if (diffHours > 0) {
126
+ return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
127
+ }
128
+ if (diffMinutes > 0) {
129
+ return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`;
130
+ }
131
+ return 'Just now';
132
+ }
133
+ /**
134
+ * Clean and sanitize message content for display.
135
+ * Converts newlines to spaces, collapses whitespace, removes non-printable chars.
136
+ *
137
+ * @param message - Raw message content
138
+ * @returns Cleaned message suitable for display
139
+ */
140
+ export function cleanMessageForTitle(message) {
141
+ return message
142
+ .replaceAll(/\n+/g, ' ')
143
+ .replaceAll(/\s+/g, ' ')
144
+ .replaceAll(/[^\u0020-\u007E]+/g, '') // Remove non-printable ASCII
145
+ .trim()
146
+ .slice(0, 100); // Limit length for title
147
+ }
@@ -1,27 +1,24 @@
1
- /**
2
- * Context data for generating context.md files.
3
- */
1
+ export interface RawConcept {
2
+ changes?: string[];
3
+ files?: string[];
4
+ flow?: string;
5
+ task?: string;
6
+ timestamp?: string;
7
+ }
8
+ export interface Narrative {
9
+ dependencies?: string;
10
+ features?: string;
11
+ structure?: string;
12
+ }
4
13
  export interface ContextData {
5
14
  name: string;
15
+ narrative?: Narrative;
16
+ rawConcept?: RawConcept;
6
17
  relations?: string[];
7
18
  snippets: string[];
8
19
  }
9
- /**
10
- * Generates Markdown files for knowledge context.
11
- */
12
20
  export declare const MarkdownWriter: {
13
- /**
14
- * Generate context.md content with snippets and optional relations.
15
- * Used for both topics and subtopics in the knowledge hierarchy.
16
- */
17
21
  generateContext(data: ContextData): string;
18
- /**
19
- * Merge two context.md contents into one.
20
- * Combines snippets and relations, deduplicating where possible.
21
- *
22
- * @param sourceContent - Raw content from source context.md
23
- * @param targetContent - Raw content from target context.md
24
- * @returns Merged context.md content
25
- */
26
22
  mergeContexts(sourceContent: string, targetContent: string): string;
23
+ parseContent(content: string, name?: string): ContextData;
27
24
  };