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,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
+ }
@@ -24,9 +24,12 @@ export class AuthToken {
24
24
  * @returns An instance of AuthToken, or undefined if required fields are missing
25
25
  */
26
26
  static fromJson(json) {
27
- // Validate that new required fields exist (for backward compatibility with old tokens)
28
- if (!json.userId || !json.userEmail) {
29
- return undefined;
27
+ // Validate ALL required fields exist (prevents corrupted/incomplete tokens from being loaded)
28
+ const requiredFields = ['accessToken', 'expiresAt', 'refreshToken', 'sessionKey', 'userEmail', 'userId'];
29
+ for (const field of requiredFields) {
30
+ if (!json[field]) {
31
+ return undefined;
32
+ }
30
33
  }
31
34
  return new AuthToken({
32
35
  accessToken: json.accessToken,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Array of all supported Events.
3
3
  */
4
- export declare const EVENT_VALUES: readonly ["repl", "auth:sign_in", "auth:signed_out", "space:init", "space:changed", "rule:generate", "mem:status", "mem:curate", "mem:pull", "mem:push", "mem:query", "onboarding:init_completed", "onboarding:curate_completed", "onboarding:query_completed", "onboarding:skipped", "onboarding:completed", "init"];
4
+ export declare const EVENT_VALUES: readonly ["repl", "auth:sign_in", "auth:signed_out", "auth:token_invalid", "space:init", "space:changed", "rule:generate", "mem:status", "mem:curate", "mem:pull", "mem:push", "mem:query", "onboarding:init_completed", "onboarding:curate_completed", "onboarding:query_completed", "onboarding:skipped", "onboarding:completed", "init"];
5
5
  export type EventName = (typeof EVENT_VALUES)[number];
6
6
  export interface PropertyDict {
7
7
  [key: string]: any;
@@ -5,6 +5,7 @@ export const EVENT_VALUES = [
5
5
  'repl',
6
6
  'auth:sign_in',
7
7
  'auth:signed_out',
8
+ 'auth:token_invalid',
8
9
  'space:init',
9
10
  'space:changed',
10
11
  'rule:generate',
@@ -18,5 +19,5 @@ export const EVENT_VALUES = [
18
19
  'onboarding:query_completed',
19
20
  'onboarding:skipped',
20
21
  'onboarding:completed',
21
- 'init'
22
+ 'init',
22
23
  ];
@@ -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
  };
@@ -1,16 +1,130 @@
1
1
  import { generateRelationsSection, parseRelations } from './relation-parser.js';
2
- /**
3
- * Extract snippets from context.md content.
4
- * Removes relations section and splits by separator.
5
- */
2
+ function generateRawConceptSection(rawConcept) {
3
+ if (!rawConcept) {
4
+ return '';
5
+ }
6
+ const parts = [];
7
+ if (rawConcept.task) {
8
+ parts.push(`**Task:**\n${rawConcept.task}`);
9
+ }
10
+ if (rawConcept.changes && rawConcept.changes.length > 0) {
11
+ parts.push(`**Changes:**\n${rawConcept.changes.map(c => `- ${c}`).join('\n')}`);
12
+ }
13
+ if (rawConcept.files && rawConcept.files.length > 0) {
14
+ parts.push(`**Files:**\n${rawConcept.files.map(f => `- ${f}`).join('\n')}`);
15
+ }
16
+ if (rawConcept.flow) {
17
+ parts.push(`**Flow:**\n${rawConcept.flow}`);
18
+ }
19
+ if (rawConcept.timestamp) {
20
+ parts.push(`**Timestamp:** ${rawConcept.timestamp}`);
21
+ }
22
+ if (parts.length === 0) {
23
+ return '';
24
+ }
25
+ return `\n## Raw Concept\n${parts.join('\n\n')}\n`;
26
+ }
27
+ function generateNarrativeSection(narrative) {
28
+ if (!narrative) {
29
+ return '';
30
+ }
31
+ const parts = [];
32
+ if (narrative.structure) {
33
+ parts.push(`### Structure\n${narrative.structure}`);
34
+ }
35
+ if (narrative.dependencies) {
36
+ parts.push(`### Dependencies\n${narrative.dependencies}`);
37
+ }
38
+ if (narrative.features) {
39
+ parts.push(`### Features\n${narrative.features}`);
40
+ }
41
+ if (parts.length === 0) {
42
+ return '';
43
+ }
44
+ return `\n## Narrative\n${parts.join('\n\n')}\n`;
45
+ }
46
+ function parseRawConceptSection(content) {
47
+ // Forgiving regex: allows optional whitespace after "## Raw Concept"
48
+ const rawConceptMatch = content.match(/##\s*Raw Concept\s*\n([\s\S]*?)(?=\n##\s|\n---\n|$)/i);
49
+ if (!rawConceptMatch) {
50
+ return undefined;
51
+ }
52
+ const sectionContent = rawConceptMatch[1];
53
+ const rawConcept = {};
54
+ // Forgiving: allows whitespace around "Task:" and after the newline
55
+ const taskMatch = sectionContent.match(/\*\*\s*Task\s*:\s*\*\*\s*\n([\s\S]*?)(?=\n\*\*|\n##|$)/i);
56
+ if (taskMatch) {
57
+ rawConcept.task = taskMatch[1].trim();
58
+ }
59
+ const changesMatch = sectionContent.match(/\*\*\s*Changes\s*:\s*\*\*\s*\n([\s\S]*?)(?=\n\*\*|\n##|$)/i);
60
+ if (changesMatch) {
61
+ rawConcept.changes = changesMatch[1]
62
+ .split('\n')
63
+ .filter(line => line.trim().startsWith('- '))
64
+ .map(line => line.trim().slice(2));
65
+ }
66
+ const filesMatch = sectionContent.match(/\*\*\s*Files\s*:\s*\*\*\s*\n([\s\S]*?)(?=\n\*\*|\n##|$)/i);
67
+ if (filesMatch) {
68
+ rawConcept.files = filesMatch[1]
69
+ .split('\n')
70
+ .filter(line => line.trim().startsWith('- '))
71
+ .map(line => line.trim().slice(2));
72
+ }
73
+ const flowMatch = sectionContent.match(/\*\*\s*Flow\s*:\s*\*\*\s*\n([\s\S]*?)(?=\n\*\*|\n##|$)/i);
74
+ if (flowMatch) {
75
+ rawConcept.flow = flowMatch[1].trim();
76
+ }
77
+ // Timestamp can be inline, so more flexible pattern
78
+ const timestampMatch = sectionContent.match(/\*\*\s*Timestamp\s*:\s*\*\*\s*(.+)/i);
79
+ if (timestampMatch) {
80
+ rawConcept.timestamp = timestampMatch[1].trim();
81
+ }
82
+ if (Object.keys(rawConcept).length === 0) {
83
+ return undefined;
84
+ }
85
+ return rawConcept;
86
+ }
87
+ function parseNarrativeSection(content) {
88
+ // Forgiving regex: allows optional whitespace after "## Narrative"
89
+ const narrativeMatch = content.match(/##\s*Narrative\s*\n([\s\S]*?)(?=\n##\s[^#]|\n---\n|$)/i);
90
+ if (!narrativeMatch) {
91
+ return undefined;
92
+ }
93
+ const sectionContent = narrativeMatch[1];
94
+ const narrative = {};
95
+ // Forgiving: allows whitespace after "### Structure"
96
+ const structureMatch = sectionContent.match(/###\s*Structure\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/i);
97
+ if (structureMatch) {
98
+ narrative.structure = structureMatch[1].trim();
99
+ }
100
+ const dependenciesMatch = sectionContent.match(/###\s*Dependencies\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/i);
101
+ if (dependenciesMatch) {
102
+ narrative.dependencies = dependenciesMatch[1].trim();
103
+ }
104
+ const featuresMatch = sectionContent.match(/###\s*Features\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/i);
105
+ if (featuresMatch) {
106
+ narrative.features = featuresMatch[1].trim();
107
+ }
108
+ if (Object.keys(narrative).length === 0) {
109
+ return undefined;
110
+ }
111
+ return narrative;
112
+ }
6
113
  function extractSnippetsFromContent(content) {
7
- // Remove relations section if present
8
114
  let snippetContent = content;
9
- const relationsMatch = content.match(/## Relations[\s\S]*?(?=\n[^@\n]|$)/);
115
+ // Forgiving regex patterns for section removal
116
+ const relationsMatch = content.match(/##\s*Relations[\s\S]*?(?=\n[^@\n]|$)/i);
10
117
  if (relationsMatch) {
11
- snippetContent = content.replace(relationsMatch[0], '').trim();
118
+ snippetContent = snippetContent.replace(relationsMatch[0], '').trim();
119
+ }
120
+ const rawConceptMatch = snippetContent.match(/##\s*Raw Concept[\s\S]*?(?=\n##\s|\n---\n|$)/i);
121
+ if (rawConceptMatch) {
122
+ snippetContent = snippetContent.replace(rawConceptMatch[0], '').trim();
123
+ }
124
+ const narrativeMatch = snippetContent.match(/##\s*Narrative[\s\S]*?(?=\n##\s|\n---\n|$)/i);
125
+ if (narrativeMatch) {
126
+ snippetContent = snippetContent.replace(narrativeMatch[0], '').trim();
12
127
  }
13
- // Split by separator and filter empty
14
128
  const snippets = snippetContent
15
129
  .split(/\n---\n/)
16
130
  .map(s => s.trim())
@@ -18,39 +132,112 @@ function extractSnippetsFromContent(content) {
18
132
  return snippets;
19
133
  }
20
134
  /**
21
- * Generates Markdown files for knowledge context.
135
+ * Merges two RawConcept objects with the following strategy:
136
+ *
137
+ * **Scalars (task, flow, timestamp)**: Source wins (source.X || target.X)
138
+ * - Rationale: The source represents "new" or "incoming" data that should
139
+ * take precedence over existing target data for singular values.
140
+ *
141
+ * **Arrays (changes, files)**: Concatenated and deduplicated (target first, then source)
142
+ * - Rationale: For lists, we want to accumulate all entries rather than
143
+ * replacing them. Target entries are placed first to preserve order.
144
+ *
145
+ * @param source - The incoming/new RawConcept to merge (takes precedence for scalars)
146
+ * @param target - The existing/base RawConcept to merge into
147
+ * @returns Merged RawConcept or undefined if both inputs are empty
22
148
  */
149
+ function mergeRawConcepts(source, target) {
150
+ if (!source && !target) {
151
+ return undefined;
152
+ }
153
+ if (!source)
154
+ return target;
155
+ if (!target)
156
+ return source;
157
+ const merged = {};
158
+ // Scalars: source wins (newer data takes precedence)
159
+ merged.task = source.task || target.task;
160
+ merged.flow = source.flow || target.flow;
161
+ merged.timestamp = source.timestamp || target.timestamp;
162
+ // Arrays: concatenate and deduplicate (target first, then source)
163
+ const allChanges = [...(target.changes || []), ...(source.changes || [])];
164
+ if (allChanges.length > 0) {
165
+ merged.changes = [...new Set(allChanges)];
166
+ }
167
+ const allFiles = [...(target.files || []), ...(source.files || [])];
168
+ if (allFiles.length > 0) {
169
+ merged.files = [...new Set(allFiles)];
170
+ }
171
+ if (Object.keys(merged).length === 0) {
172
+ return undefined;
173
+ }
174
+ return merged;
175
+ }
176
+ function mergeNarratives(source, target) {
177
+ if (!source && !target) {
178
+ return undefined;
179
+ }
180
+ if (!source)
181
+ return target;
182
+ if (!target)
183
+ return source;
184
+ const merged = {};
185
+ if (source.structure || target.structure) {
186
+ const parts = [target.structure, source.structure].filter(Boolean);
187
+ merged.structure = parts.join('\n\n');
188
+ }
189
+ if (source.dependencies || target.dependencies) {
190
+ const parts = [target.dependencies, source.dependencies].filter(Boolean);
191
+ merged.dependencies = parts.join('\n\n');
192
+ }
193
+ if (source.features || target.features) {
194
+ const parts = [target.features, source.features].filter(Boolean);
195
+ merged.features = parts.join('\n\n');
196
+ }
197
+ if (Object.keys(merged).length === 0) {
198
+ return undefined;
199
+ }
200
+ return merged;
201
+ }
23
202
  export const MarkdownWriter = {
24
- /**
25
- * Generate context.md content with snippets and optional relations.
26
- * Used for both topics and subtopics in the knowledge hierarchy.
27
- */
28
203
  generateContext(data) {
29
- const snippets = data.snippets || [];
204
+ const snippets = (data.snippets || []).filter(s => s && s.trim());
30
205
  const relations = data.relations || [];
31
206
  const relationsSection = generateRelationsSection(relations);
32
- return `${relationsSection}
33
- ${snippets.length > 0 ? snippets.map(s => `${s}`).join('\n\n---\n\n') : 'No context available.'}
34
- `;
207
+ const rawConceptSection = generateRawConceptSection(data.rawConcept);
208
+ const narrativeSection = generateNarrativeSection(data.narrative);
209
+ const hasSnippets = snippets.length > 0;
210
+ // Build the content parts
211
+ const parts = [];
212
+ // Add sections (relations, rawConcept, narrative)
213
+ const sectionsContent = `${relationsSection}${rawConceptSection}${narrativeSection}`.trim();
214
+ if (sectionsContent) {
215
+ parts.push(sectionsContent);
216
+ }
217
+ // Add snippets if present
218
+ if (hasSnippets) {
219
+ const snippetsContent = snippets.join('\n\n---\n\n');
220
+ parts.push(snippetsContent);
221
+ }
222
+ // If nothing at all, return empty (should not happen in practice)
223
+ if (parts.length === 0) {
224
+ return '';
225
+ }
226
+ // Join parts with separator only if we have both sections and snippets
227
+ return parts.join('\n\n---\n\n') + '\n';
35
228
  },
36
- /**
37
- * Merge two context.md contents into one.
38
- * Combines snippets and relations, deduplicating where possible.
39
- *
40
- * @param sourceContent - Raw content from source context.md
41
- * @param targetContent - Raw content from target context.md
42
- * @returns Merged context.md content
43
- */
44
229
  mergeContexts(sourceContent, targetContent) {
45
- // Extract relations from both contents
46
230
  const sourceRelations = parseRelations(sourceContent);
47
231
  const targetRelations = parseRelations(targetContent);
48
- // Merge and deduplicate relations
49
232
  const mergedRelations = [...new Set([...sourceRelations, ...targetRelations])];
233
+ const sourceRawConcept = parseRawConceptSection(sourceContent);
234
+ const targetRawConcept = parseRawConceptSection(targetContent);
235
+ const mergedRawConcept = mergeRawConcepts(sourceRawConcept, targetRawConcept);
236
+ const sourceNarrative = parseNarrativeSection(sourceContent);
237
+ const targetNarrative = parseNarrativeSection(targetContent);
238
+ const mergedNarrative = mergeNarratives(sourceNarrative, targetNarrative);
50
239
  const sourceSnippets = extractSnippetsFromContent(sourceContent);
51
240
  const targetSnippets = extractSnippetsFromContent(targetContent);
52
- // Merge snippets (target first, then source)
53
- // Deduplicate by exact match
54
241
  const seenSnippets = new Set();
55
242
  const mergedSnippets = [];
56
243
  for (const snippet of [...targetSnippets, ...sourceSnippets]) {
@@ -59,10 +246,21 @@ ${snippets.length > 0 ? snippets.map(s => `${s}`).join('\n\n---\n\n') : 'No cont
59
246
  mergedSnippets.push(snippet);
60
247
  }
61
248
  }
62
- // Generate merged content
63
- const relationsSection = generateRelationsSection(mergedRelations);
64
- return `${relationsSection}
65
- ${mergedSnippets.length > 0 ? mergedSnippets.join('\n\n---\n\n') : 'No context available.'}
66
- `;
249
+ return MarkdownWriter.generateContext({
250
+ name: '',
251
+ narrative: mergedNarrative,
252
+ rawConcept: mergedRawConcept,
253
+ relations: mergedRelations,
254
+ snippets: mergedSnippets,
255
+ });
256
+ },
257
+ parseContent(content, name = '') {
258
+ return {
259
+ name,
260
+ narrative: parseNarrativeSection(content),
261
+ rawConcept: parseRawConceptSection(content),
262
+ relations: parseRelations(content),
263
+ snippets: extractSnippetsFromContent(content),
264
+ };
67
265
  },
68
266
  };
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Utilities for parsing and managing context relations.
3
- * Relations are expressed using @ notation: @domain/topic or @domain/topic/subtopic
3
+ * Relations are expressed using @ notation: @domain/topic/title.md or @domain/topic/subtopic/title.md
4
4
  */
5
5
  /**
6
- * Parse relations from context.md content.
7
- * Extracts all @domain/topic or @domain/topic/subtopic references.
6
+ * Parse relations from title.md content.
7
+ * Extracts all @domain/topic/title.md or @domain/topic/subtopic/title.md references.
8
8
  *
9
9
  * @param content - Markdown content to parse
10
10
  * @returns Array of unique relation paths (without @ prefix)
@@ -13,43 +13,27 @@
13
13
  * ```ts
14
14
  * const content = `
15
15
  * ## Relations
16
- * @code_style/error-handling
17
- * @structure/api-endpoints
16
+ * @code_style/error-handling/overview.md
17
+ * @structure/api/endpoints/rest.md
18
18
  * `
19
- * parseRelations(content) // ['code_style/error-handling', 'structure/api-endpoints']
19
+ * parseRelations(content) // ['code_style/error-handling/overview.md', 'structure/api/endpoints/rest.md']
20
20
  * ```
21
21
  */
22
22
  export declare function parseRelations(content: string): string[];
23
- /**
24
- * Validate a relation path format.
25
- * Valid formats: domain/topic or domain/topic/subtopic
26
- *
27
- * @param path - Relation path to validate (without @ prefix)
28
- * @returns True if path format is valid
29
- *
30
- * @example
31
- * ```ts
32
- * validateRelationPath('code_style/error-handling') // true
33
- * validateRelationPath('code_style/error-handling/try-catch') // true
34
- * validateRelationPath('invalid') // false
35
- * validateRelationPath('too/many/parts/here') // false
36
- * ```
37
- */
38
- export declare function validateRelationPath(path: string): boolean;
39
23
  /**
40
24
  * Resolve a relation path to an absolute file system path.
41
25
  *
42
26
  * @param basePath - Base path to context tree (e.g., '.brv/context-tree')
43
- * @param relation - Relation path (e.g., 'domain/topic' or 'domain/topic/subtopic')
44
- * @returns Absolute path to the context.md file
27
+ * @param relation - Relation path (e.g., 'domain/topic/title.md' or 'domain/topic/subtopic/title.md')
28
+ * @returns Absolute path to the title.md file
45
29
  *
46
30
  * @example
47
31
  * ```ts
48
- * resolveRelationPath('.brv/context-tree', 'code_style/error-handling')
49
- * // => '.brv/context-tree/code_style/error-handling/context.md'
32
+ * resolveRelationPath('.brv/context-tree', 'code_style/error-handling/overview.md')
33
+ * // => '.brv/context-tree/code_style/error-handling/overview.md'
50
34
  *
51
- * resolveRelationPath('.brv/context-tree', 'structure/api/endpoints')
52
- * // => '.brv/context-tree/structure/api/endpoints/context.md'
35
+ * resolveRelationPath('.brv/context-tree', 'structure/api/endpoints/rest.md')
36
+ * // => '.brv/context-tree/structure/api/endpoints/rest.md'
53
37
  * ```
54
38
  */
55
39
  export declare function resolveRelationPath(basePath: string, relation: string): string;
@@ -58,30 +42,47 @@ export declare function resolveRelationPath(basePath: string, relation: string):
58
42
  *
59
43
  * @param domain - Domain name
60
44
  * @param topic - Topic name
45
+ * @param title - Title (with .md extension)
61
46
  * @param subtopic - Optional subtopic name
62
47
  * @returns Formatted relation string with @ prefix
63
48
  *
64
49
  * @example
65
50
  * ```ts
66
- * formatRelation('code_style', 'error-handling')
67
- * // => '@code_style/error-handling'
51
+ * formatRelation('code_style', 'error-handling', 'overview.md')
52
+ * // => '@code_style/error-handling/overview.md'
68
53
  *
69
- * formatRelation('structure', 'api', 'endpoints')
70
- * // => '@structure/api/endpoints'
54
+ * formatRelation('structure', 'api', 'endpoints', 'rest.md')
55
+ * // => '@structure/api/endpoints/rest.md'
56
+ * ```
57
+ */
58
+ export declare function formatRelation(domain: string, topic: string, title: string, subtopic?: string): string;
59
+ /**
60
+ * Normalize a relation path by removing the @ prefix.
61
+ * Preserves file extensions (e.g., .md).
62
+ *
63
+ * @param relation - Relation path to normalize
64
+ * @returns Normalized relation path without @ prefix (file extension preserved)
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * normalizeRelation('code_style/error-handling.md') // 'code_style/error-handling.md'
69
+ * normalizeRelation('@code_style/error-handling.md') // 'code_style/error-handling.md'
70
+ * normalizeRelation('code_style/error-handling/title.md') // 'code_style/error-handling/title.md'
71
+ * normalizeRelation('code_style/error-handling/file.md') // 'code_style/error-handling/file.md'
71
72
  * ```
72
73
  */
73
- export declare function formatRelation(domain: string, topic: string, subtopic?: string): string;
74
+ export declare function normalizeRelation(relation: string): string;
74
75
  /**
75
76
  * Generate the Relations section for context.md.
76
77
  * Returns empty string if no relations provided.
77
78
  *
78
- * @param relations - Array of relation paths (without @ prefix)
79
+ * @param relations - Array of relation paths (with or without @ prefix)
79
80
  * @returns Markdown formatted Relations section or empty string
80
81
  *
81
82
  * @example
82
83
  * ```ts
83
- * generateRelationsSection(['code_style/error-handling', 'structure/api'])
84
- * // => '\n## Relations\n@code_style/error-handling\n@structure/api\n'
84
+ * generateRelationsSection(['code_style/error-handling/overview.md', 'structure/api/rest.md'])
85
+ * // => '\n## Relations\n@code_style/error-handling/overview.md\n@structure/api/rest.md\n'
85
86
  *
86
87
  * generateRelationsSection([])
87
88
  * // => ''