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,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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
16
|
+
* @code_style/error-handling/overview.md
|
|
17
|
+
* @structure/api/endpoints/rest.md
|
|
18
18
|
* `
|
|
19
|
-
* parseRelations(content) // ['code_style/error-handling', 'structure/api
|
|
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
|
|
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/
|
|
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/
|
|
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
|
|
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
|
* // => ''
|