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,77 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Model Types - Shared types for model providers
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type { Message, ContentBlock, ToolUseContent, TextContent, SessionId, ToolCallId } from '../types/session.js';
|
|
6
|
+
import type { ToolDefinition } from '../types/tools.js';
|
|
7
|
+
|
|
8
|
+
/** Options for a model completion request */
|
|
9
|
+
export interface ModelRequestOptions {
|
|
10
|
+
model: string;
|
|
11
|
+
maxTokens: number;
|
|
12
|
+
temperature: number;
|
|
13
|
+
tools: ToolDefinition[];
|
|
14
|
+
sessionId: SessionId;
|
|
15
|
+
stopSequences?: string[];
|
|
16
|
+
systemPrompt?: string;
|
|
17
|
+
/** Control thinking mode: enabled|disabled|auto */
|
|
18
|
+
thinking?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Response from a model completion request */
|
|
22
|
+
export interface ModelResponse {
|
|
23
|
+
/** The assistant's message content blocks */
|
|
24
|
+
content: ContentBlock[];
|
|
25
|
+
/** The model that generated the response */
|
|
26
|
+
model: string;
|
|
27
|
+
/** Stop reason */
|
|
28
|
+
stopReason: 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence';
|
|
29
|
+
/** Token usage */
|
|
30
|
+
usage: TokenUsage;
|
|
31
|
+
/** The full session ID */
|
|
32
|
+
sessionId: SessionId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Token usage information */
|
|
36
|
+
export interface TokenUsage {
|
|
37
|
+
inputTokens: number;
|
|
38
|
+
outputTokens: number;
|
|
39
|
+
cacheReadTokens?: number;
|
|
40
|
+
cacheWriteTokens?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Streaming event types */
|
|
44
|
+
export type StreamEvent =
|
|
45
|
+
| { type: 'text_delta'; delta: string }
|
|
46
|
+
| { type: 'thinking_delta'; delta: string }
|
|
47
|
+
| { type: 'tool_call_start'; toolCallId: string; toolName: string }
|
|
48
|
+
| { type: 'tool_call_delta'; toolCallId: string; delta: string }
|
|
49
|
+
| { type: 'tool_call_complete'; toolCallId: string; toolName: string; input: Record<string, unknown> }
|
|
50
|
+
| { type: 'message_start'; model: string }
|
|
51
|
+
| { type: 'message_complete'; stopReason: ModelResponse['stopReason']; usage: TokenUsage }
|
|
52
|
+
| { type: 'error'; error: unknown };
|
|
53
|
+
|
|
54
|
+
/** Interface that all model providers must implement */
|
|
55
|
+
export interface ModelProvider {
|
|
56
|
+
/** Provider display name */
|
|
57
|
+
readonly name: string;
|
|
58
|
+
|
|
59
|
+
/** Non-streaming completion */
|
|
60
|
+
complete(messages: Message[], options: ModelRequestOptions): Promise<ModelResponse>;
|
|
61
|
+
|
|
62
|
+
/** Streaming completion */
|
|
63
|
+
stream(messages: Message[], options: ModelRequestOptions): AsyncGenerator<StreamEvent>;
|
|
64
|
+
|
|
65
|
+
/** Token counting (approximate) */
|
|
66
|
+
countTokens(messages: Message[]): Promise<number>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Helper to create a text content block */
|
|
70
|
+
export function textContent(text: string): TextContent {
|
|
71
|
+
return { type: 'text', text };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Helper to create a tool use content block */
|
|
75
|
+
export function toolUseContent(id: string, name: string, input: Record<string, unknown>): ToolUseContent {
|
|
76
|
+
return { type: 'tool_use', id: id as ToolCallId, name, input };
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApprovalManager.d.ts","sourceRoot":"","sources":["ApprovalManager.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC3F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,WAAW,sBAAsB;IACrC,4BAA4B;IAC5B,WAAW,EAAE,YAAY,CAAC;IAC1B,yCAAyC;IACzC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mDAAmD;IACnD,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,kDAAkD;IAClD,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,4BAA4B;IAC5B,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC;IACzD,oCAAoC;IACpC,eAAe,EAAE,OAAO,CAAC;IACzB,0BAA0B;IAC1B,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;CACvD;AAED,MAAM,WAAW,eAAe;IAC9B,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACvD;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,kBAAkB,CAAU;IACpC,OAAO,CAAC,qBAAqB,CAAc;IAC3C,OAAO,CAAC,oBAAoB,CAAc;IAC1C,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,eAAe,CAAsC;IAC7D,OAAO,CAAC,OAAO,CAAgC;gBAEnC,OAAO,EAAE,sBAAsB;IAQ3C,6CAA6C;IAC7C,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAI1C,+BAA+B;IAC/B,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAIjC,oCAAoC;IACpC,OAAO,IAAI,YAAY;IAIvB,6CAA6C;IAC7C,gBAAgB,CACd,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO;IA2CV,mDAAmD;IACnD,YAAY,CACV,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,cAAc,CAAC,EAAE,cAAc,GAC9B,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU;IAWzC,4CAA4C;IACtC,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsB1E,oCAAoC;IACpC,kBAAkB,IAAI,eAAe,EAAE;IAIvC,+BAA+B;IAC/B,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAItC,mCAAmC;IACnC,OAAO,CAAC,YAAY;CAOrB"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ApprovalManager - Manages tool approval workflow
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type { ApprovalRequest, ApprovalResponse, ApprovalMode } from '../types/session.js';
|
|
6
|
+
import type { ToolDefinition } from '../types/tools.js';
|
|
7
|
+
import { ApprovalError } from '../types/errors.js';
|
|
8
|
+
|
|
9
|
+
export interface ApprovalManagerOptions {
|
|
10
|
+
/** Default approval mode */
|
|
11
|
+
defaultMode: ApprovalMode;
|
|
12
|
+
/** Auto-approve tools with 'low' risk */
|
|
13
|
+
autoApproveLowRisk?: boolean;
|
|
14
|
+
/** Always require approval for these tool names */
|
|
15
|
+
alwaysRequireApproval?: string[];
|
|
16
|
+
/** Never require approval for these tool names */
|
|
17
|
+
neverRequireApproval?: string[];
|
|
18
|
+
/** Custom approval rules */
|
|
19
|
+
rules?: ApprovalRule[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ApprovalRule {
|
|
23
|
+
/** Tool name pattern (glob) */
|
|
24
|
+
toolPattern: string;
|
|
25
|
+
/** Input field conditions */
|
|
26
|
+
conditions?: Record<string, (value: unknown) => boolean>;
|
|
27
|
+
/** Override approval requirement */
|
|
28
|
+
requireApproval: boolean;
|
|
29
|
+
/** Override risk level */
|
|
30
|
+
riskOverride?: 'low' | 'medium' | 'high' | 'critical';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ApprovalHandler {
|
|
34
|
+
(request: ApprovalRequest): Promise<ApprovalResponse>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ApprovalManager {
|
|
38
|
+
private mode: ApprovalMode;
|
|
39
|
+
private autoApproveLowRisk: boolean;
|
|
40
|
+
private alwaysRequireApproval: Set<string>;
|
|
41
|
+
private neverRequireApproval: Set<string>;
|
|
42
|
+
private rules: ApprovalRule[];
|
|
43
|
+
private pendingRequests = new Map<string, ApprovalRequest>();
|
|
44
|
+
private handler: ApprovalHandler | null = null;
|
|
45
|
+
|
|
46
|
+
constructor(options: ApprovalManagerOptions) {
|
|
47
|
+
this.mode = options.defaultMode;
|
|
48
|
+
this.autoApproveLowRisk = options.autoApproveLowRisk ?? false;
|
|
49
|
+
this.alwaysRequireApproval = new Set(options.alwaysRequireApproval || []);
|
|
50
|
+
this.neverRequireApproval = new Set(options.neverRequireApproval || []);
|
|
51
|
+
this.rules = options.rules || [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Set the approval handler (UI callback) */
|
|
55
|
+
setHandler(handler: ApprovalHandler): void {
|
|
56
|
+
this.handler = handler;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Update the approval mode */
|
|
60
|
+
setMode(mode: ApprovalMode): void {
|
|
61
|
+
this.mode = mode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Get the current approval mode */
|
|
65
|
+
getMode(): ApprovalMode {
|
|
66
|
+
return this.mode;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Check if a tool call requires approval */
|
|
70
|
+
requiresApproval(
|
|
71
|
+
toolName: string,
|
|
72
|
+
toolInput: Record<string, unknown>,
|
|
73
|
+
toolDefinition?: ToolDefinition
|
|
74
|
+
): boolean {
|
|
75
|
+
// YOLO mode: never require approval
|
|
76
|
+
if (this.mode === 'yolo') return false;
|
|
77
|
+
|
|
78
|
+
// Never-approve list
|
|
79
|
+
if (this.neverRequireApproval.has(toolName)) return false;
|
|
80
|
+
|
|
81
|
+
// Always-approve list
|
|
82
|
+
if (this.alwaysRequireApproval.has(toolName)) return true;
|
|
83
|
+
|
|
84
|
+
// Check custom rules
|
|
85
|
+
for (const rule of this.rules) {
|
|
86
|
+
if (this.matchPattern(toolName, rule.toolPattern)) {
|
|
87
|
+
if (rule.conditions) {
|
|
88
|
+
const matchesAll = Object.entries(rule.conditions).every(
|
|
89
|
+
([field, check]) => check(toolInput[field])
|
|
90
|
+
);
|
|
91
|
+
if (matchesAll) {
|
|
92
|
+
return rule.requireApproval;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
return rule.requireApproval;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Tool definition check
|
|
101
|
+
if (toolDefinition) {
|
|
102
|
+
if (typeof toolDefinition.requiresApproval === 'boolean') {
|
|
103
|
+
return toolDefinition.requiresApproval;
|
|
104
|
+
}
|
|
105
|
+
return toolDefinition.requiresApproval(toolInput, this.mode);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Default: require approval in plan mode, skip for low risk in accepting_edits
|
|
109
|
+
if (this.mode === 'plan') return true;
|
|
110
|
+
if (this.mode === 'accepting_edits') return false;
|
|
111
|
+
if (this.mode === 'smart') return true; // Smart mode asks for important ops
|
|
112
|
+
|
|
113
|
+
// Default mode: require approval for high/critical risk tools
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Get the effective risk level for a tool call */
|
|
118
|
+
getRiskLevel(
|
|
119
|
+
toolName: string,
|
|
120
|
+
toolInput: Record<string, unknown>,
|
|
121
|
+
toolDefinition?: ToolDefinition
|
|
122
|
+
): 'low' | 'medium' | 'high' | 'critical' {
|
|
123
|
+
// Check custom rules for risk override
|
|
124
|
+
for (const rule of this.rules) {
|
|
125
|
+
if (rule.riskOverride && this.matchPattern(toolName, rule.toolPattern)) {
|
|
126
|
+
return rule.riskOverride;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return toolDefinition?.riskLevel || 'medium';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Request user approval for a tool call */
|
|
134
|
+
async requestApproval(request: ApprovalRequest): Promise<ApprovalResponse> {
|
|
135
|
+
if (!this.handler) {
|
|
136
|
+
// No handler set, auto-approve based on mode
|
|
137
|
+
if (this.mode === 'yolo' || (this.autoApproveLowRisk && request.risk === 'low')) {
|
|
138
|
+
return { requestId: request.id, approved: true };
|
|
139
|
+
}
|
|
140
|
+
throw new ApprovalError('No approval handler configured and approval is required');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.pendingRequests.set(request.id, request);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const response = await this.handler(request);
|
|
147
|
+
if (!response.requestId) {
|
|
148
|
+
response.requestId = request.id;
|
|
149
|
+
}
|
|
150
|
+
return response;
|
|
151
|
+
} finally {
|
|
152
|
+
this.pendingRequests.delete(request.id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Get pending approval requests */
|
|
157
|
+
getPendingRequests(): ApprovalRequest[] {
|
|
158
|
+
return Array.from(this.pendingRequests.values());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Cancel a pending request */
|
|
162
|
+
cancelRequest(requestId: string): void {
|
|
163
|
+
this.pendingRequests.delete(requestId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Simple glob pattern matching */
|
|
167
|
+
private matchPattern(name: string, pattern: string): boolean {
|
|
168
|
+
const regexStr = pattern
|
|
169
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
170
|
+
.replace(/\*/g, '.*')
|
|
171
|
+
.replace(/\?/g, '.');
|
|
172
|
+
return new RegExp(`^${regexStr}$`).test(name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileFilter.d.ts","sourceRoot":"","sources":["FileFilter.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,gBAAgB;IAC/B,8BAA8B;IAC9B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,+BAA+B;IAC/B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uDAAuD;IACvD,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,cAAc,CAAW;IACjC,OAAO,CAAC,aAAa,CAAW;IAChC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,gBAAgB,CAAS;gBAErB,MAAM,EAAE,gBAAgB;IAWpC,sCAAsC;IACtC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAkBlE,mDAAmD;IACnD,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAiCpC,4CAA4C;IACtC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAgB/F,gCAAgC;IAChC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAW/F,wEAAwE;IACxE,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IAQ1C,2BAA2B;IAC3B,OAAO,CAAC,SAAS;CAOlB"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// FileFilter - Controls which files can be accessed by tools
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
|
|
8
|
+
export interface FileFilterConfig {
|
|
9
|
+
/** Glob patterns to ignore */
|
|
10
|
+
ignorePatterns: string[];
|
|
11
|
+
/** Patterns to always allow */
|
|
12
|
+
allowPatterns?: string[];
|
|
13
|
+
/** Maximum file size in bytes */
|
|
14
|
+
maxFileSize?: number;
|
|
15
|
+
/** Maximum batch size for operations */
|
|
16
|
+
maxBatchSize?: number;
|
|
17
|
+
/** Forbidden paths (absolute) */
|
|
18
|
+
forbiddenPaths?: string[];
|
|
19
|
+
/** Working directory (for relative path resolution) */
|
|
20
|
+
workingDirectory: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class FileFilter {
|
|
24
|
+
private ignorePatterns: string[];
|
|
25
|
+
private allowPatterns: string[];
|
|
26
|
+
private maxFileSize: number;
|
|
27
|
+
private maxBatchSize: number;
|
|
28
|
+
private forbiddenPaths: Set<string>;
|
|
29
|
+
private workingDirectory: string;
|
|
30
|
+
|
|
31
|
+
constructor(config: FileFilterConfig) {
|
|
32
|
+
this.ignorePatterns = config.ignorePatterns;
|
|
33
|
+
this.allowPatterns = config.allowPatterns || [];
|
|
34
|
+
this.maxFileSize = config.maxFileSize || 10 * 1024 * 1024; // 10MB
|
|
35
|
+
this.maxBatchSize = config.maxBatchSize || 100;
|
|
36
|
+
this.forbiddenPaths = new Set(
|
|
37
|
+
(config.forbiddenPaths || []).map((p) => path.resolve(p))
|
|
38
|
+
);
|
|
39
|
+
this.workingDirectory = config.workingDirectory;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Check if a file path is allowed */
|
|
43
|
+
isAllowed(filePath: string): { allowed: boolean; reason?: string } {
|
|
44
|
+
const resolved = path.resolve(filePath);
|
|
45
|
+
|
|
46
|
+
// Check forbidden paths
|
|
47
|
+
for (const forbidden of this.forbiddenPaths) {
|
|
48
|
+
if (resolved.startsWith(forbidden) || resolved === forbidden) {
|
|
49
|
+
return { allowed: false, reason: `Path is in forbidden directory: ${forbidden}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if it's within the working directory
|
|
54
|
+
if (!resolved.startsWith(this.workingDirectory) && !path.isAbsolute(resolved)) {
|
|
55
|
+
return { allowed: false, reason: `Path is outside the working directory` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { allowed: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Check if a file path matches ignore patterns */
|
|
62
|
+
isIgnored(filePath: string): boolean {
|
|
63
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
64
|
+
|
|
65
|
+
for (const pattern of this.ignorePatterns) {
|
|
66
|
+
if (this.matchGlob(normalized, pattern)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Standard ignores
|
|
72
|
+
const alwaysIgnore = [
|
|
73
|
+
'node_modules',
|
|
74
|
+
'.git',
|
|
75
|
+
'dist',
|
|
76
|
+
'build',
|
|
77
|
+
'.next',
|
|
78
|
+
'.nuxt',
|
|
79
|
+
'__pycache__',
|
|
80
|
+
'.venv',
|
|
81
|
+
'venv',
|
|
82
|
+
'.env',
|
|
83
|
+
'.env.local',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
for (const dir of alwaysIgnore) {
|
|
87
|
+
if (normalized.includes(`/${dir}/`) || normalized.includes(`\\${dir}\\`)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Check if a file is within size limits */
|
|
96
|
+
async checkFileSize(filePath: string): Promise<{ ok: boolean; size?: number; reason?: string }> {
|
|
97
|
+
try {
|
|
98
|
+
const stat = await fs.stat(filePath);
|
|
99
|
+
if (stat.size > this.maxFileSize) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
size: stat.size,
|
|
103
|
+
reason: `File size (${stat.size} bytes) exceeds maximum (${this.maxFileSize} bytes)`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, size: stat.size };
|
|
107
|
+
} catch {
|
|
108
|
+
return { ok: false, reason: 'Cannot stat file' };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Validate a batch of files */
|
|
113
|
+
validateBatch(filePaths: string[]): { valid: boolean; invalidFiles: string[]; reason?: string } {
|
|
114
|
+
if (filePaths.length > this.maxBatchSize) {
|
|
115
|
+
return {
|
|
116
|
+
valid: false,
|
|
117
|
+
invalidFiles: filePaths.slice(this.maxBatchSize),
|
|
118
|
+
reason: `Batch size (${filePaths.length}) exceeds maximum (${this.maxBatchSize})`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { valid: false, invalidFiles: [] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Filter a list of file paths, removing ignored and disallowed ones */
|
|
125
|
+
filterPaths(filePaths: string[]): string[] {
|
|
126
|
+
return filePaths.filter((p) => {
|
|
127
|
+
const allowed = this.isAllowed(p);
|
|
128
|
+
if (!allowed.allowed) return false;
|
|
129
|
+
return !this.isIgnored(p);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Simple glob matching */
|
|
134
|
+
private matchGlob(str: string, pattern: string): boolean {
|
|
135
|
+
const regexStr = pattern
|
|
136
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
137
|
+
.replace(/\*/g, '.*')
|
|
138
|
+
.replace(/\?/g, '.');
|
|
139
|
+
return new RegExp(`^${regexStr}$`).test(str);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HookExecutor.d.ts","sourceRoot":"","sources":["HookExecutor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAG7E,MAAM,WAAW,mBAAmB;IAClC,8CAA8C;IAC9C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wBAAwB;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,4BAA4B;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,gCAAgC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,YAAa,SAAQ,YAAY;IAC5C,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,OAAO,CAAU;gBAEb,OAAO,EAAE,mBAAmB;IAQxC,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAQlC,8BAA8B;IAC9B,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI;IAMxC,0CAA0C;IACpC,OAAO,CACX,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CAAC,UAAU,EAAE,CAAC;IAgCxB,4BAA4B;YACd,WAAW;IA2DzB,6CAA6C;IAC7C,OAAO,CAAC,iBAAiB;IAUzB,0DAA0D;IAC1D,OAAO,CAAC,cAAc;IAStB,2BAA2B;IAC3B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIlC,+BAA+B;IAC/B,QAAQ,IAAI,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC;CAGtC"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// HookExecutor - Executes lifecycle hooks
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
import type { HookConfig, HookResult, HookEvent } from '../types/session.js';
|
|
8
|
+
import { HookError, TimeoutError } from '../types/errors.js';
|
|
9
|
+
|
|
10
|
+
export interface HookExecutorOptions {
|
|
11
|
+
/** Default timeout for hook execution (ms) */
|
|
12
|
+
defaultTimeout?: number;
|
|
13
|
+
/** Working directory */
|
|
14
|
+
workingDirectory: string;
|
|
15
|
+
/** Environment variables */
|
|
16
|
+
environment?: Record<string, string>;
|
|
17
|
+
/** Whether hooks are enabled */
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class HookExecutor extends EventEmitter {
|
|
22
|
+
private hooks = new Map<string, HookConfig[]>();
|
|
23
|
+
private defaultTimeout: number;
|
|
24
|
+
private workingDirectory: string;
|
|
25
|
+
private environment: Record<string, string>;
|
|
26
|
+
private enabled: boolean;
|
|
27
|
+
|
|
28
|
+
constructor(options: HookExecutorOptions) {
|
|
29
|
+
super();
|
|
30
|
+
this.defaultTimeout = options.defaultTimeout || 30000;
|
|
31
|
+
this.workingDirectory = options.workingDirectory;
|
|
32
|
+
this.environment = options.environment || {};
|
|
33
|
+
this.enabled = options.enabled ?? true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Register a hook configuration */
|
|
37
|
+
register(config: HookConfig): void {
|
|
38
|
+
const event = config.event;
|
|
39
|
+
if (!this.hooks.has(event)) {
|
|
40
|
+
this.hooks.set(event, []);
|
|
41
|
+
}
|
|
42
|
+
this.hooks.get(event)!.push(config);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Register multiple hooks */
|
|
46
|
+
registerAll(configs: HookConfig[]): void {
|
|
47
|
+
for (const config of configs) {
|
|
48
|
+
this.register(config);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Execute all hooks for a given event */
|
|
53
|
+
async execute(
|
|
54
|
+
event: HookEvent,
|
|
55
|
+
context?: Record<string, unknown>
|
|
56
|
+
): Promise<HookResult[]> {
|
|
57
|
+
if (!this.enabled) return [];
|
|
58
|
+
|
|
59
|
+
const hooks = this.hooks.get(event) || [];
|
|
60
|
+
const results: HookResult[] = [];
|
|
61
|
+
|
|
62
|
+
for (const hook of hooks) {
|
|
63
|
+
// Check condition
|
|
64
|
+
if (hook.condition) {
|
|
65
|
+
try {
|
|
66
|
+
const meets = this.evaluateCondition(hook.condition, context || {});
|
|
67
|
+
if (!meets) continue;
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = await this.executeHook(hook, context);
|
|
74
|
+
results.push(result);
|
|
75
|
+
|
|
76
|
+
this.emit('hook_executed', { event, hook, result });
|
|
77
|
+
|
|
78
|
+
// If hook failed, stop chain
|
|
79
|
+
if (result.exitCode !== 0) {
|
|
80
|
+
this.emit('hook_failed', { event, hook, result });
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Execute a single hook */
|
|
89
|
+
private async executeHook(hook: HookConfig, context?: Record<string, unknown>): Promise<HookResult> {
|
|
90
|
+
const timeout = hook.timeout || this.defaultTimeout;
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
|
|
93
|
+
return new Promise<HookResult>((resolve) => {
|
|
94
|
+
// Build environment with context variables
|
|
95
|
+
const env: Record<string, string> = {
|
|
96
|
+
...this.environment,
|
|
97
|
+
NOVA_HOOK_EVENT: hook.event,
|
|
98
|
+
NOVA_WORKING_DIR: this.workingDirectory,
|
|
99
|
+
...(context ? this.flattenContext(context) : {}),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const proc = spawn('sh', ['-c', hook.command], {
|
|
103
|
+
cwd: this.workingDirectory,
|
|
104
|
+
env: { ...process.env, ...env },
|
|
105
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
106
|
+
windowsHide: true,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let stdout = '';
|
|
110
|
+
let stderr = '';
|
|
111
|
+
|
|
112
|
+
proc.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
|
|
113
|
+
proc.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
|
|
114
|
+
proc.stdin.end();
|
|
115
|
+
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
proc.kill('SIGKILL');
|
|
118
|
+
resolve({
|
|
119
|
+
exitCode: -1,
|
|
120
|
+
stdout,
|
|
121
|
+
stderr: stderr || `Hook timed out after ${timeout}ms`,
|
|
122
|
+
duration: Date.now() - startTime,
|
|
123
|
+
});
|
|
124
|
+
}, timeout);
|
|
125
|
+
|
|
126
|
+
proc.on('close', (code) => {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
resolve({
|
|
129
|
+
exitCode: code ?? -1,
|
|
130
|
+
stdout,
|
|
131
|
+
stderr,
|
|
132
|
+
duration: Date.now() - startTime,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
proc.on('error', (err) => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
resolve({
|
|
139
|
+
exitCode: -1,
|
|
140
|
+
stdout: '',
|
|
141
|
+
stderr: err.message,
|
|
142
|
+
duration: Date.now() - startTime,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Evaluate a simple condition expression */
|
|
149
|
+
private evaluateCondition(condition: string, context: Record<string, unknown>): boolean {
|
|
150
|
+
// Support simple conditions like "tool == write_file" or "risk == critical"
|
|
151
|
+
const match = condition.match(/^(\w+)\s*==\s*(\w+)$/);
|
|
152
|
+
if (match) {
|
|
153
|
+
const [, key, value] = match;
|
|
154
|
+
return String(context[key]) === value;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Flatten context object into NOVA_ prefixed env vars */
|
|
160
|
+
private flattenContext(context: Record<string, unknown>): Record<string, string> {
|
|
161
|
+
const result: Record<string, string> = {};
|
|
162
|
+
for (const [key, value] of Object.entries(context)) {
|
|
163
|
+
const envKey = `NOVA_${key.toUpperCase().replace(/[^A-Z0-9_]/g, '_')}`;
|
|
164
|
+
result[envKey] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Enable/disable hooks */
|
|
170
|
+
setEnabled(enabled: boolean): void {
|
|
171
|
+
this.enabled = enabled;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Get all registered hooks */
|
|
175
|
+
getHooks(): Map<string, HookConfig[]> {
|
|
176
|
+
return new Map(this.hooks);
|
|
177
|
+
}
|
|
178
|
+
}
|