nova-terminal-assistant 0.1.0
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.
Potentially problematic release.
This version of nova-terminal-assistant might be problematic. Click here for more details.
- package/README.md +358 -0
- package/bin/nova +38 -0
- package/bin/nova.js +12 -0
- package/package.json +67 -0
- package/src/cli/commands/SmartCompletion.ts +458 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/startup/IFlowRepl.ts +212 -0
- package/src/cli/startup/InkBasedRepl.ts +1056 -0
- package/src/cli/startup/InteractiveRepl.ts +2833 -0
- package/src/cli/startup/NovaApp.ts +1861 -0
- package/src/cli/startup/index.ts +4 -0
- package/src/cli/startup/parseArgs.ts +293 -0
- package/src/cli/test-modules.ts +27 -0
- package/src/cli/ui/IFlowDropdown.ts +425 -0
- package/src/cli/ui/ModernReplUI.ts +276 -0
- package/src/cli/ui/SimpleSelector2.ts +215 -0
- package/src/cli/ui/components/ConfirmDialog.ts +176 -0
- package/src/cli/ui/components/ErrorPanel.ts +364 -0
- package/src/cli/ui/components/InkAppRunner.tsx +67 -0
- package/src/cli/ui/components/InkComponents.tsx +613 -0
- package/src/cli/ui/components/NovaInkApp.tsx +312 -0
- package/src/cli/ui/components/ProgressBar.ts +177 -0
- package/src/cli/ui/components/ProgressIndicator.ts +298 -0
- package/src/cli/ui/components/QuickActions.ts +396 -0
- package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
- package/src/cli/ui/components/StatusBar.ts +194 -0
- package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
- package/src/cli/ui/components/index.ts +27 -0
- package/src/cli/ui/ink-prototype.tsx +347 -0
- package/src/cli/utils/CliUI.ts +336 -0
- package/src/cli/utils/CompletionHelper.ts +388 -0
- package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
- package/src/cli/utils/EnhancedCompleter.ts +513 -0
- package/src/cli/utils/ErrorEnhancer.ts +429 -0
- package/src/cli/utils/OutputFormatter.ts +193 -0
- package/src/cli/utils/index.ts +9 -0
- package/src/core/agents/AgentOrchestrator.ts +515 -0
- package/src/core/agents/index.ts +17 -0
- package/src/core/audit/AuditLogger.ts +509 -0
- package/src/core/audit/index.ts +11 -0
- package/src/core/auth/AuthManager.d.ts.map +1 -0
- package/src/core/auth/AuthManager.ts +138 -0
- package/src/core/auth/index.d.ts.map +1 -0
- package/src/core/auth/index.ts +2 -0
- package/src/core/config/ConfigManager.d.ts.map +1 -0
- package/src/core/config/ConfigManager.test.ts +183 -0
- package/src/core/config/ConfigManager.ts +1219 -0
- package/src/core/config/index.d.ts.map +1 -0
- package/src/core/config/index.ts +1 -0
- package/src/core/context/ContextBuilder.d.ts.map +1 -0
- package/src/core/context/ContextBuilder.ts +171 -0
- package/src/core/context/ContextCompressor.d.ts.map +1 -0
- package/src/core/context/ContextCompressor.ts +642 -0
- package/src/core/context/LayeredMemoryManager.ts +657 -0
- package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
- package/src/core/context/MemoryDiscovery.ts +175 -0
- package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
- package/src/core/context/defaultSystemPrompt.ts +35 -0
- package/src/core/context/index.d.ts.map +1 -0
- package/src/core/context/index.ts +22 -0
- package/src/core/extensions/SkillGenerator.ts +421 -0
- package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
- package/src/core/extensions/SkillInstaller.ts +257 -0
- package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
- package/src/core/extensions/SkillRegistry.ts +361 -0
- package/src/core/extensions/SkillValidator.ts +525 -0
- package/src/core/extensions/index.ts +15 -0
- package/src/core/index.d.ts.map +1 -0
- package/src/core/index.ts +42 -0
- package/src/core/mcp/McpManager.d.ts.map +1 -0
- package/src/core/mcp/McpManager.ts +632 -0
- package/src/core/mcp/index.d.ts.map +1 -0
- package/src/core/mcp/index.ts +2 -0
- package/src/core/model/ModelClient.d.ts.map +1 -0
- package/src/core/model/ModelClient.ts +217 -0
- package/src/core/model/ModelConnectionTester.ts +363 -0
- package/src/core/model/ModelValidator.ts +348 -0
- package/src/core/model/index.d.ts.map +1 -0
- package/src/core/model/index.ts +6 -0
- package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
- package/src/core/model/providers/AnthropicProvider.ts +279 -0
- package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
- package/src/core/model/providers/CodingPlanProvider.ts +210 -0
- package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
- package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
- package/src/core/model/providers/OllamaManager.ts +201 -0
- package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaProvider.ts +73 -0
- package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
- package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAIProvider.ts +29 -0
- package/src/core/model/providers/index.d.ts.map +1 -0
- package/src/core/model/providers/index.ts +12 -0
- package/src/core/model/types.d.ts.map +1 -0
- package/src/core/model/types.ts +77 -0
- package/src/core/security/ApprovalManager.d.ts.map +1 -0
- package/src/core/security/ApprovalManager.ts +174 -0
- package/src/core/security/FileFilter.d.ts.map +1 -0
- package/src/core/security/FileFilter.ts +141 -0
- package/src/core/security/HookExecutor.d.ts.map +1 -0
- package/src/core/security/HookExecutor.ts +178 -0
- package/src/core/security/SandboxExecutor.ts +447 -0
- package/src/core/security/index.d.ts.map +1 -0
- package/src/core/security/index.ts +8 -0
- package/src/core/session/AgentLoop.d.ts.map +1 -0
- package/src/core/session/AgentLoop.ts +501 -0
- package/src/core/session/SessionManager.d.ts.map +1 -0
- package/src/core/session/SessionManager.test.ts +183 -0
- package/src/core/session/SessionManager.ts +460 -0
- package/src/core/session/index.d.ts.map +1 -0
- package/src/core/session/index.ts +3 -0
- package/src/core/telemetry/Telemetry.d.ts.map +1 -0
- package/src/core/telemetry/Telemetry.ts +90 -0
- package/src/core/telemetry/TelemetryService.ts +531 -0
- package/src/core/telemetry/index.d.ts.map +1 -0
- package/src/core/telemetry/index.ts +12 -0
- package/src/core/testing/AutoFixer.ts +385 -0
- package/src/core/testing/ErrorAnalyzer.ts +499 -0
- package/src/core/testing/TestRunner.ts +265 -0
- package/src/core/testing/agent-cli-tests.ts +538 -0
- package/src/core/testing/index.ts +11 -0
- package/src/core/tools/ToolRegistry.d.ts.map +1 -0
- package/src/core/tools/ToolRegistry.test.ts +206 -0
- package/src/core/tools/ToolRegistry.ts +260 -0
- package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/EditFileTool.ts +97 -0
- package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
- package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/MemoryTool.ts +102 -0
- package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/ReadFileTool.ts +58 -0
- package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchContentTool.ts +94 -0
- package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchFileTool.ts +61 -0
- package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
- package/src/core/tools/impl/ShellTool.ts +118 -0
- package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
- package/src/core/tools/impl/TaskTool.ts +207 -0
- package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
- package/src/core/tools/impl/TodoTool.ts +122 -0
- package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebFetchTool.ts +103 -0
- package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebSearchTool.ts +89 -0
- package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/WriteFileTool.ts +49 -0
- package/src/core/tools/impl/index.d.ts.map +1 -0
- package/src/core/tools/impl/index.ts +16 -0
- package/src/core/tools/index.d.ts.map +1 -0
- package/src/core/tools/index.ts +7 -0
- package/src/core/tools/schemas/execution.d.ts.map +1 -0
- package/src/core/tools/schemas/execution.ts +42 -0
- package/src/core/tools/schemas/file.d.ts.map +1 -0
- package/src/core/tools/schemas/file.ts +119 -0
- package/src/core/tools/schemas/index.d.ts.map +1 -0
- package/src/core/tools/schemas/index.ts +11 -0
- package/src/core/tools/schemas/memory.d.ts.map +1 -0
- package/src/core/tools/schemas/memory.ts +52 -0
- package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
- package/src/core/tools/schemas/orchestration.ts +44 -0
- package/src/core/tools/schemas/search.d.ts.map +1 -0
- package/src/core/tools/schemas/search.ts +112 -0
- package/src/core/tools/schemas/todo.d.ts.map +1 -0
- package/src/core/tools/schemas/todo.ts +32 -0
- package/src/core/tools/schemas/web.d.ts.map +1 -0
- package/src/core/tools/schemas/web.ts +86 -0
- package/src/core/types/config.d.ts.map +1 -0
- package/src/core/types/config.ts +200 -0
- package/src/core/types/errors.d.ts.map +1 -0
- package/src/core/types/errors.ts +204 -0
- package/src/core/types/index.d.ts.map +1 -0
- package/src/core/types/index.ts +8 -0
- package/src/core/types/session.d.ts.map +1 -0
- package/src/core/types/session.ts +216 -0
- package/src/core/types/tools.d.ts.map +1 -0
- package/src/core/types/tools.ts +157 -0
- package/src/core/utils/CheckpointManager.d.ts.map +1 -0
- package/src/core/utils/CheckpointManager.ts +327 -0
- package/src/core/utils/Logger.d.ts.map +1 -0
- package/src/core/utils/Logger.ts +98 -0
- package/src/core/utils/RetryManager.ts +471 -0
- package/src/core/utils/TokenCounter.d.ts.map +1 -0
- package/src/core/utils/TokenCounter.ts +414 -0
- package/src/core/utils/VectorMemoryStore.ts +440 -0
- package/src/core/utils/helpers.d.ts.map +1 -0
- package/src/core/utils/helpers.ts +89 -0
- package/src/core/utils/index.d.ts.map +1 -0
- package/src/core/utils/index.ts +19 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// AuditLogger - Comprehensive audit logging system
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { createLogger } from '../utils/Logger.js';
|
|
8
|
+
import { generateId } from '../utils/helpers.js';
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('AuditLogger');
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Audit action types
|
|
18
|
+
*/
|
|
19
|
+
export type AuditAction =
|
|
20
|
+
| 'tool_use'
|
|
21
|
+
| 'file_read'
|
|
22
|
+
| 'file_write'
|
|
23
|
+
| 'file_delete'
|
|
24
|
+
| 'command_exec'
|
|
25
|
+
| 'model_call'
|
|
26
|
+
| 'session_start'
|
|
27
|
+
| 'session_end'
|
|
28
|
+
| 'approval_granted'
|
|
29
|
+
| 'approval_denied'
|
|
30
|
+
| 'config_change'
|
|
31
|
+
| 'auth_change';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Audit log entry
|
|
35
|
+
*/
|
|
36
|
+
export interface AuditEntry {
|
|
37
|
+
id: string;
|
|
38
|
+
timestamp: Date;
|
|
39
|
+
action: AuditAction;
|
|
40
|
+
actor: 'user' | 'agent' | 'system';
|
|
41
|
+
resource: string;
|
|
42
|
+
result: 'success' | 'denied' | 'error' | 'timeout';
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
metadata: {
|
|
45
|
+
model?: string;
|
|
46
|
+
provider?: string;
|
|
47
|
+
tokens?: { input: number; output: number };
|
|
48
|
+
duration?: number;
|
|
49
|
+
reason?: string;
|
|
50
|
+
error?: string;
|
|
51
|
+
input?: Record<string, unknown>;
|
|
52
|
+
output?: string;
|
|
53
|
+
diff?: string;
|
|
54
|
+
ip?: string;
|
|
55
|
+
userAgent?: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Audit query filter
|
|
61
|
+
*/
|
|
62
|
+
export interface AuditFilter {
|
|
63
|
+
startTime?: Date;
|
|
64
|
+
endTime?: Date;
|
|
65
|
+
actions?: AuditAction[];
|
|
66
|
+
actors?: ('user' | 'agent' | 'system')[];
|
|
67
|
+
results?: AuditEntry['result'][];
|
|
68
|
+
sessionId?: string;
|
|
69
|
+
resourcePattern?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Audit configuration
|
|
74
|
+
*/
|
|
75
|
+
export interface AuditConfig {
|
|
76
|
+
/** Log file path */
|
|
77
|
+
logFile: string;
|
|
78
|
+
/** Maximum log file size in bytes (default 10MB) */
|
|
79
|
+
maxFileSize?: number;
|
|
80
|
+
/** Maximum number of log files to keep (default 10) */
|
|
81
|
+
maxFiles?: number;
|
|
82
|
+
/** Whether to log to console (default false) */
|
|
83
|
+
console?: boolean;
|
|
84
|
+
/** Minimum log level */
|
|
85
|
+
minLevel?: 'debug' | 'info' | 'warn' | 'error';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// AuditLogger
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Comprehensive audit logging system
|
|
94
|
+
*/
|
|
95
|
+
export class AuditLogger {
|
|
96
|
+
private config: Required<AuditConfig>;
|
|
97
|
+
private writeQueue: AuditEntry[] = [];
|
|
98
|
+
private isWriting = false;
|
|
99
|
+
private flushInterval: NodeJS.Timeout | null = null;
|
|
100
|
+
|
|
101
|
+
constructor(config: AuditConfig) {
|
|
102
|
+
this.config = {
|
|
103
|
+
maxFileSize: config.maxFileSize ?? 10 * 1024 * 1024, // 10MB
|
|
104
|
+
maxFiles: config.maxFiles ?? 10,
|
|
105
|
+
console: config.console ?? false,
|
|
106
|
+
minLevel: config.minLevel ?? 'info',
|
|
107
|
+
...config,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Ensure log directory exists
|
|
111
|
+
const logDir = path.dirname(this.config.logFile);
|
|
112
|
+
if (!fs.existsSync(logDir)) {
|
|
113
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Start periodic flush
|
|
117
|
+
this.flushInterval = setInterval(() => this.flush(), 5000);
|
|
118
|
+
|
|
119
|
+
// Flush on exit
|
|
120
|
+
process.on('beforeExit', () => this.flush());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// -----------------------------------------------------------------------
|
|
124
|
+
// Core Logging
|
|
125
|
+
// -----------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Log an audit entry
|
|
129
|
+
*/
|
|
130
|
+
async log(entry: Omit<AuditEntry, 'id' | 'timestamp'>): Promise<AuditEntry> {
|
|
131
|
+
const fullEntry: AuditEntry = {
|
|
132
|
+
...entry,
|
|
133
|
+
id: generateId(),
|
|
134
|
+
timestamp: new Date(),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Add to write queue
|
|
138
|
+
this.writeQueue.push(fullEntry);
|
|
139
|
+
|
|
140
|
+
// Console output
|
|
141
|
+
if (this.config.console) {
|
|
142
|
+
this.logToConsole(fullEntry);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Flush if queue is getting large
|
|
146
|
+
if (this.writeQueue.length >= 100) {
|
|
147
|
+
await this.flush();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return fullEntry;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Log a tool use event
|
|
155
|
+
*/
|
|
156
|
+
async logToolUse(
|
|
157
|
+
toolName: string,
|
|
158
|
+
input: Record<string, unknown>,
|
|
159
|
+
result: AuditEntry['result'],
|
|
160
|
+
metadata: Partial<AuditEntry['metadata']> = {}
|
|
161
|
+
): Promise<AuditEntry> {
|
|
162
|
+
return this.log({
|
|
163
|
+
action: 'tool_use',
|
|
164
|
+
actor: 'agent',
|
|
165
|
+
resource: toolName,
|
|
166
|
+
result,
|
|
167
|
+
metadata: {
|
|
168
|
+
input,
|
|
169
|
+
...metadata,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Log a file operation
|
|
176
|
+
*/
|
|
177
|
+
async logFileOperation(
|
|
178
|
+
operation: 'file_read' | 'file_write' | 'file_delete',
|
|
179
|
+
filePath: string,
|
|
180
|
+
result: AuditEntry['result'],
|
|
181
|
+
metadata: Partial<AuditEntry['metadata']> = {}
|
|
182
|
+
): Promise<AuditEntry> {
|
|
183
|
+
return this.log({
|
|
184
|
+
action: operation,
|
|
185
|
+
actor: 'agent',
|
|
186
|
+
resource: filePath,
|
|
187
|
+
result,
|
|
188
|
+
metadata,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Log a command execution
|
|
194
|
+
*/
|
|
195
|
+
async logCommand(
|
|
196
|
+
command: string,
|
|
197
|
+
result: AuditEntry['result'],
|
|
198
|
+
metadata: Partial<AuditEntry['metadata']> = {}
|
|
199
|
+
): Promise<AuditEntry> {
|
|
200
|
+
return this.log({
|
|
201
|
+
action: 'command_exec',
|
|
202
|
+
actor: 'agent',
|
|
203
|
+
resource: command,
|
|
204
|
+
result,
|
|
205
|
+
metadata,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Log a model API call
|
|
211
|
+
*/
|
|
212
|
+
async logModelCall(
|
|
213
|
+
model: string,
|
|
214
|
+
provider: string,
|
|
215
|
+
result: AuditEntry['result'],
|
|
216
|
+
metadata: Partial<AuditEntry['metadata']> = {}
|
|
217
|
+
): Promise<AuditEntry> {
|
|
218
|
+
return this.log({
|
|
219
|
+
action: 'model_call',
|
|
220
|
+
actor: 'agent',
|
|
221
|
+
resource: `${provider}/${model}`,
|
|
222
|
+
result,
|
|
223
|
+
metadata: {
|
|
224
|
+
model,
|
|
225
|
+
provider,
|
|
226
|
+
...metadata,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Log an approval decision
|
|
233
|
+
*/
|
|
234
|
+
async logApproval(
|
|
235
|
+
resource: string,
|
|
236
|
+
granted: boolean,
|
|
237
|
+
reason?: string
|
|
238
|
+
): Promise<AuditEntry> {
|
|
239
|
+
return this.log({
|
|
240
|
+
action: granted ? 'approval_granted' : 'approval_denied',
|
|
241
|
+
actor: 'user',
|
|
242
|
+
resource,
|
|
243
|
+
result: granted ? 'success' : 'denied',
|
|
244
|
+
metadata: { reason },
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Log a session event
|
|
250
|
+
*/
|
|
251
|
+
async logSessionEvent(
|
|
252
|
+
event: 'session_start' | 'session_end',
|
|
253
|
+
sessionId: string,
|
|
254
|
+
metadata: Partial<AuditEntry['metadata']> = {}
|
|
255
|
+
): Promise<AuditEntry> {
|
|
256
|
+
return this.log({
|
|
257
|
+
action: event,
|
|
258
|
+
actor: 'system',
|
|
259
|
+
resource: sessionId,
|
|
260
|
+
result: 'success',
|
|
261
|
+
sessionId,
|
|
262
|
+
metadata,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// -----------------------------------------------------------------------
|
|
267
|
+
// Querying
|
|
268
|
+
// -----------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Query audit logs
|
|
272
|
+
*/
|
|
273
|
+
async query(filter: AuditFilter = {}): Promise<AuditEntry[]> {
|
|
274
|
+
await this.flush(); // Ensure all entries are written
|
|
275
|
+
|
|
276
|
+
const entries: AuditEntry[] = [];
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
// Read all log files
|
|
280
|
+
const logDir = path.dirname(this.config.logFile);
|
|
281
|
+
const baseName = path.basename(this.config.logFile, '.jsonl');
|
|
282
|
+
const files = fs.readdirSync(logDir)
|
|
283
|
+
.filter(f => f.startsWith(baseName))
|
|
284
|
+
.sort()
|
|
285
|
+
.reverse(); // Most recent first
|
|
286
|
+
|
|
287
|
+
for (const file of files) {
|
|
288
|
+
const filePath = path.join(logDir, file);
|
|
289
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
290
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
291
|
+
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
try {
|
|
294
|
+
const entry = JSON.parse(line) as AuditEntry;
|
|
295
|
+
entry.timestamp = new Date(entry.timestamp);
|
|
296
|
+
|
|
297
|
+
if (this.matchesFilter(entry, filter)) {
|
|
298
|
+
entries.push(entry);
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
// Skip malformed lines
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
logger.error('Failed to query audit logs', { error });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return entries;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Check if entry matches filter
|
|
314
|
+
*/
|
|
315
|
+
private matchesFilter(entry: AuditEntry, filter: AuditFilter): boolean {
|
|
316
|
+
if (filter.startTime && entry.timestamp < filter.startTime) return false;
|
|
317
|
+
if (filter.endTime && entry.timestamp > filter.endTime) return false;
|
|
318
|
+
if (filter.actions && !filter.actions.includes(entry.action)) return false;
|
|
319
|
+
if (filter.actors && !filter.actors.includes(entry.actor)) return false;
|
|
320
|
+
if (filter.results && !filter.results.includes(entry.result)) return false;
|
|
321
|
+
if (filter.sessionId && entry.sessionId !== filter.sessionId) return false;
|
|
322
|
+
if (filter.resourcePattern && !entry.resource.match(new RegExp(filter.resourcePattern, 'i'))) return false;
|
|
323
|
+
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// -----------------------------------------------------------------------
|
|
328
|
+
// Export
|
|
329
|
+
// -----------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Export logs in various formats
|
|
333
|
+
*/
|
|
334
|
+
async export(format: 'json' | 'csv' | 'txt', filter: AuditFilter = {}): Promise<string> {
|
|
335
|
+
const entries = await this.query(filter);
|
|
336
|
+
|
|
337
|
+
if (format === 'json') {
|
|
338
|
+
return JSON.stringify(entries, null, 2);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (format === 'csv') {
|
|
342
|
+
const headers = ['id', 'timestamp', 'action', 'actor', 'resource', 'result', 'sessionId'];
|
|
343
|
+
const rows = entries.map(e => [
|
|
344
|
+
e.id,
|
|
345
|
+
e.timestamp.toISOString(),
|
|
346
|
+
e.action,
|
|
347
|
+
e.actor,
|
|
348
|
+
`"${e.resource.replace(/"/g, '""')}"`,
|
|
349
|
+
e.result,
|
|
350
|
+
e.sessionId ?? '',
|
|
351
|
+
].join(','));
|
|
352
|
+
return [headers.join(','), ...rows].join('\n');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// format === 'txt'
|
|
356
|
+
return entries.map(e => {
|
|
357
|
+
const time = e.timestamp.toISOString();
|
|
358
|
+
return `[${time}] ${e.action.toUpperCase()} ${e.actor} ${e.resource} (${e.result})`;
|
|
359
|
+
}).join('\n');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// -----------------------------------------------------------------------
|
|
363
|
+
// Maintenance
|
|
364
|
+
// -----------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Flush write queue to disk
|
|
368
|
+
*/
|
|
369
|
+
async flush(): Promise<void> {
|
|
370
|
+
if (this.isWriting || this.writeQueue.length === 0) return;
|
|
371
|
+
|
|
372
|
+
this.isWriting = true;
|
|
373
|
+
const entries = [...this.writeQueue];
|
|
374
|
+
this.writeQueue = [];
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
// Check if log rotation is needed
|
|
378
|
+
await this.rotateIfNeeded();
|
|
379
|
+
|
|
380
|
+
// Append entries
|
|
381
|
+
const lines = entries.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
382
|
+
fs.appendFileSync(this.config.logFile, lines, 'utf-8');
|
|
383
|
+
|
|
384
|
+
logger.debug(`Flushed ${entries.length} audit entries`);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
logger.error('Failed to flush audit entries', { error });
|
|
387
|
+
// Put entries back in queue
|
|
388
|
+
this.writeQueue.unshift(...entries);
|
|
389
|
+
} finally {
|
|
390
|
+
this.isWriting = false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Rotate log file if needed
|
|
396
|
+
*/
|
|
397
|
+
private async rotateIfNeeded(): Promise<void> {
|
|
398
|
+
try {
|
|
399
|
+
if (!fs.existsSync(this.config.logFile)) return;
|
|
400
|
+
|
|
401
|
+
const stats = fs.statSync(this.config.logFile);
|
|
402
|
+
|
|
403
|
+
if (stats.size >= this.config.maxFileSize) {
|
|
404
|
+
const logDir = path.dirname(this.config.logFile);
|
|
405
|
+
const baseName = path.basename(this.config.logFile, '.jsonl');
|
|
406
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
407
|
+
const newName = `${baseName}.${timestamp}.jsonl`;
|
|
408
|
+
|
|
409
|
+
fs.renameSync(this.config.logFile, path.join(logDir, newName));
|
|
410
|
+
|
|
411
|
+
// Clean up old files
|
|
412
|
+
const files = fs.readdirSync(logDir)
|
|
413
|
+
.filter(f => f.startsWith(baseName) && f.endsWith('.jsonl'))
|
|
414
|
+
.sort();
|
|
415
|
+
|
|
416
|
+
while (files.length > this.config.maxFiles) {
|
|
417
|
+
const oldFile = files.shift()!;
|
|
418
|
+
fs.unlinkSync(path.join(logDir, oldFile));
|
|
419
|
+
logger.debug(`Deleted old audit log: ${oldFile}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
logger.info(`Rotated audit log to ${newName}`);
|
|
423
|
+
}
|
|
424
|
+
} catch (error) {
|
|
425
|
+
logger.error('Failed to rotate audit log', { error });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get statistics about the audit log
|
|
431
|
+
*/
|
|
432
|
+
async getStats(): Promise<{
|
|
433
|
+
totalEntries: number;
|
|
434
|
+
oldestEntry: Date | null;
|
|
435
|
+
newestEntry: Date | null;
|
|
436
|
+
byAction: Record<string, number>;
|
|
437
|
+
byResult: Record<string, number>;
|
|
438
|
+
}> {
|
|
439
|
+
const entries = await this.query();
|
|
440
|
+
|
|
441
|
+
const byAction: Record<string, number> = {};
|
|
442
|
+
const byResult: Record<string, number> = {};
|
|
443
|
+
let oldest: Date | null = null;
|
|
444
|
+
let newest: Date | null = null;
|
|
445
|
+
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
byAction[entry.action] = (byAction[entry.action] ?? 0) + 1;
|
|
448
|
+
byResult[entry.result] = (byResult[entry.result] ?? 0) + 1;
|
|
449
|
+
|
|
450
|
+
if (!oldest || entry.timestamp < oldest) oldest = entry.timestamp;
|
|
451
|
+
if (!newest || entry.timestamp > newest) newest = entry.timestamp;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
totalEntries: entries.length,
|
|
456
|
+
oldestEntry: oldest,
|
|
457
|
+
newestEntry: newest,
|
|
458
|
+
byAction,
|
|
459
|
+
byResult,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Log to console
|
|
465
|
+
*/
|
|
466
|
+
private logToConsole(entry: AuditEntry): void {
|
|
467
|
+
const time = entry.timestamp.toISOString();
|
|
468
|
+
const level = entry.result === 'error' ? 'error' : entry.result === 'denied' ? 'warn' : 'info';
|
|
469
|
+
const message = `[AUDIT] ${time} ${entry.action} ${entry.actor} ${entry.resource} (${entry.result})`;
|
|
470
|
+
|
|
471
|
+
switch (level) {
|
|
472
|
+
case 'error':
|
|
473
|
+
console.error(message);
|
|
474
|
+
break;
|
|
475
|
+
case 'warn':
|
|
476
|
+
console.warn(message);
|
|
477
|
+
break;
|
|
478
|
+
default:
|
|
479
|
+
console.log(message);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Shutdown the logger
|
|
485
|
+
*/
|
|
486
|
+
shutdown(): void {
|
|
487
|
+
if (this.flushInterval) {
|
|
488
|
+
clearInterval(this.flushInterval);
|
|
489
|
+
this.flushInterval = null;
|
|
490
|
+
}
|
|
491
|
+
this.flush();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ============================================================================
|
|
496
|
+
// Factory function
|
|
497
|
+
// ============================================================================
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Create an audit logger with default configuration
|
|
501
|
+
*/
|
|
502
|
+
export function createAuditLogger(
|
|
503
|
+
storageDir?: string
|
|
504
|
+
): AuditLogger {
|
|
505
|
+
const defaultDir = storageDir ?? path.join(process.env.HOME ?? '~', '.nova', 'logs');
|
|
506
|
+
return new AuditLogger({
|
|
507
|
+
logFile: path.join(defaultDir, 'audit.jsonl'),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Audit Module - Audit logging system
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export { AuditLogger, createAuditLogger } from './AuditLogger.js';
|
|
6
|
+
export type {
|
|
7
|
+
AuditAction,
|
|
8
|
+
AuditEntry,
|
|
9
|
+
AuditFilter,
|
|
10
|
+
AuditConfig,
|
|
11
|
+
} from './AuditLogger.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthManager.d.ts","sourceRoot":"","sources":["AuthManager.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,WAAW,CAAsC;;IAMzD,oCAAoC;IAC9B,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtC,qCAAqC;IACrC,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAiBtG,gDAAgD;IAChD,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAKzC,qCAAqC;IAC/B,cAAc,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAe3H,wCAAwC;IAClC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAM3D,oCAAoC;IACpC,aAAa,IAAI,MAAM,EAAE;IAIzB,kCAAkC;YACpB,eAAe;IAiB7B,OAAO,CAAC,aAAa;CA6BtB"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// AuthManager - Manages API credentials
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
|
|
9
|
+
export interface CredentialEntry {
|
|
10
|
+
provider: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
organizationId?: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class AuthManager {
|
|
19
|
+
private credentialsPath: string;
|
|
20
|
+
private credentials = new Map<string, CredentialEntry>();
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
this.credentialsPath = path.join(os.homedir(), '.nova', 'credentials.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Load credentials from storage */
|
|
27
|
+
async loadCredentials(): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
const content = await fs.readFile(this.credentialsPath, 'utf-8');
|
|
30
|
+
const data = JSON.parse(content) as Record<string, CredentialEntry>;
|
|
31
|
+
for (const [provider, entry] of Object.entries(data)) {
|
|
32
|
+
this.credentials.set(provider, entry);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// No credentials file yet
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Get credentials for a provider */
|
|
40
|
+
getCredentials(provider: string): { apiKey: string; baseUrl?: string; organizationId?: string } | null {
|
|
41
|
+
const entry = this.credentials.get(provider);
|
|
42
|
+
if (!entry) {
|
|
43
|
+
const envKey = this.getEnvKeyName(provider);
|
|
44
|
+
const envValue = process.env[envKey];
|
|
45
|
+
if (envValue) {
|
|
46
|
+
return { apiKey: envValue };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
apiKey: entry.apiKey,
|
|
52
|
+
baseUrl: entry.baseUrl,
|
|
53
|
+
organizationId: entry.organizationId,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Check if credentials exist for a provider */
|
|
58
|
+
hasCredentials(provider: string): boolean {
|
|
59
|
+
if (this.credentials.has(provider)) return true;
|
|
60
|
+
return !!process.env[this.getEnvKeyName(provider)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Set credentials for a provider */
|
|
64
|
+
async setCredentials(entry: { provider: string; apiKey: string; baseUrl?: string; organizationId?: string }): Promise<void> {
|
|
65
|
+
const existing = this.credentials.get(entry.provider);
|
|
66
|
+
const credential: CredentialEntry = {
|
|
67
|
+
provider: entry.provider,
|
|
68
|
+
apiKey: entry.apiKey,
|
|
69
|
+
baseUrl: entry.baseUrl,
|
|
70
|
+
organizationId: entry.organizationId,
|
|
71
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
72
|
+
updatedAt: Date.now(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
this.credentials.set(entry.provider, credential);
|
|
76
|
+
await this.saveCredentials();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Remove credentials for a provider */
|
|
80
|
+
async removeCredentials(provider: string): Promise<boolean> {
|
|
81
|
+
const deleted = this.credentials.delete(provider);
|
|
82
|
+
if (deleted) await this.saveCredentials();
|
|
83
|
+
return deleted;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** List all configured providers */
|
|
87
|
+
listProviders(): string[] {
|
|
88
|
+
return Array.from(this.credentials.keys());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Save credentials to storage */
|
|
92
|
+
private async saveCredentials(): Promise<void> {
|
|
93
|
+
const dir = path.dirname(this.credentialsPath);
|
|
94
|
+
await fs.mkdir(dir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
const data: Record<string, CredentialEntry> = {};
|
|
97
|
+
for (const [provider, entry] of this.credentials) {
|
|
98
|
+
data[provider] = entry;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await fs.writeFile(this.credentialsPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
102
|
+
try {
|
|
103
|
+
await fs.chmod(this.credentialsPath, 0o600);
|
|
104
|
+
} catch {
|
|
105
|
+
// chmod may fail on some systems
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private getEnvKeyName(provider: string): string {
|
|
110
|
+
const envMap: Record<string, string> = {
|
|
111
|
+
// Built-in
|
|
112
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
113
|
+
openai: 'OPENAI_API_KEY',
|
|
114
|
+
azure: 'AZURE_OPENAI_API_KEY',
|
|
115
|
+
ollama: 'OLLAMA_HOST',
|
|
116
|
+
'ollama-cloud': 'OLLAMA_API_KEY',
|
|
117
|
+
// Major cloud providers
|
|
118
|
+
google: 'GOOGLE_API_KEY',
|
|
119
|
+
deepseek: 'DEEPSEEK_API_KEY',
|
|
120
|
+
// Chinese providers
|
|
121
|
+
qwen: 'DASHSCOPE_API_KEY',
|
|
122
|
+
glm: 'ZHIPU_API_KEY',
|
|
123
|
+
zhipu: 'ZHIPU_API_KEY',
|
|
124
|
+
moonshot: 'MOONSHOT_API_KEY',
|
|
125
|
+
baichuan: 'BAICHUAN_API_KEY',
|
|
126
|
+
minimax: 'MINIMAX_API_KEY',
|
|
127
|
+
yi: 'YI_API_KEY',
|
|
128
|
+
siliconflow: 'SILICONFLOW_API_KEY',
|
|
129
|
+
// International providers
|
|
130
|
+
groq: 'GROQ_API_KEY',
|
|
131
|
+
mistral: 'MISTRAL_API_KEY',
|
|
132
|
+
together: 'TOGETHER_API_KEY',
|
|
133
|
+
cohere: 'COHERE_API_KEY',
|
|
134
|
+
perplexity: 'PERPLEXITY_API_KEY',
|
|
135
|
+
};
|
|
136
|
+
return envMap[provider] || `${provider.toUpperCase().replace(/-/g, '_')}_API_KEY`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfigManager.d.ts","sourceRoot":"","sources":["ConfigManager.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA+7BnG,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,iBAAiB,CAAuB;;IAMhD,0DAA0D;IACpD,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAqCpD,mCAAmC;IACnC,SAAS,IAAI,UAAU;IAOvB,sBAAsB;IACtB,aAAa,IAAI,UAAU;IAI3B,8BAA8B;IAC9B,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,mBAAmB,CAAC;QAAC,KAAK,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI;IA6C7F;;;;OAIG;IACH,aAAa,CACX,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,WAAW,EACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAC/C,OAAO;IAuBV;;OAEG;IACH,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,mBAAmB,GAAG,OAAO;IAMpF;;OAEG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAQzD,2BAA2B;IAC3B,aAAa,IAAI,MAAM;IAIvB,4BAA4B;IAC5B,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKtC,yBAAyB;IACnB,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAOtD,oCAAoC;IACpC,OAAO,CAAC,WAAW;IAYnB,2CAA2C;IAC3C,OAAO,CAAC,iBAAiB;IAkBzB,+CAA+C;IAC/C,OAAO,CAAC,WAAW;IA6BnB,2BAA2B;IAC3B,OAAO,CAAC,UAAU;CAWnB"}
|