neuro-cli 4.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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type PermissionMode = 'manual' | 'auto' | 'plan' | 'yolo';
|
|
2
|
+
export interface ApprovalResult {
|
|
3
|
+
approved: boolean;
|
|
4
|
+
remember: boolean;
|
|
5
|
+
always?: boolean;
|
|
6
|
+
edited?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ApprovalConfig {
|
|
9
|
+
mode: PermissionMode;
|
|
10
|
+
/** Tools that are auto-approved without asking */
|
|
11
|
+
whitelist: string[];
|
|
12
|
+
/** Tools that are always denied */
|
|
13
|
+
blacklist: string[];
|
|
14
|
+
/** Whether to show diff preview before file modifications */
|
|
15
|
+
showDiffPreview: boolean;
|
|
16
|
+
/** Whether to batch-approve similar operations */
|
|
17
|
+
batchApprove: boolean;
|
|
18
|
+
/** Maximum number of consecutive approvals before re-asking (0 = unlimited) */
|
|
19
|
+
maxConsecutiveAutoApproves: number;
|
|
20
|
+
/** Whether to persist "always" decisions across sessions */
|
|
21
|
+
persistDecisions: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface ToolApprovalStats {
|
|
24
|
+
toolName: string;
|
|
25
|
+
approved: number;
|
|
26
|
+
denied: number;
|
|
27
|
+
avgResponseTimeMs: number;
|
|
28
|
+
}
|
|
29
|
+
export declare class ApprovalSystem {
|
|
30
|
+
private mode;
|
|
31
|
+
private sessionApprovals;
|
|
32
|
+
private deniedTools;
|
|
33
|
+
private persistentApprovals;
|
|
34
|
+
private whitelist;
|
|
35
|
+
private blacklist;
|
|
36
|
+
private showDiffPreview;
|
|
37
|
+
private batchApprove;
|
|
38
|
+
private maxConsecutiveAutoApproves;
|
|
39
|
+
private persistDecisions;
|
|
40
|
+
private consecutiveAutoApproves;
|
|
41
|
+
private rl;
|
|
42
|
+
private stats;
|
|
43
|
+
private pendingBatch;
|
|
44
|
+
private batchTimer;
|
|
45
|
+
private autoApprovePatterns;
|
|
46
|
+
constructor(mode?: PermissionMode, config?: Partial<ApprovalConfig>);
|
|
47
|
+
setMode(mode: PermissionMode): void;
|
|
48
|
+
getMode(): PermissionMode;
|
|
49
|
+
cycleMode(): PermissionMode;
|
|
50
|
+
getModeDescription(): string;
|
|
51
|
+
getModeIcon(): string;
|
|
52
|
+
addToWhitelist(toolName: string): void;
|
|
53
|
+
removeFromWhitelist(toolName: string): void;
|
|
54
|
+
addToBlacklist(toolName: string): void;
|
|
55
|
+
removeFromBlacklist(toolName: string): void;
|
|
56
|
+
getWhitelist(): string[];
|
|
57
|
+
getBlacklist(): string[];
|
|
58
|
+
requestApproval(toolName: string, args: Record<string, unknown>, risk: 'low' | 'medium' | 'high', description?: string): Promise<ApprovalResult>;
|
|
59
|
+
private isFileModification;
|
|
60
|
+
private promptWithDiffPreview;
|
|
61
|
+
private promptUser;
|
|
62
|
+
requestBatchApproval(toolName: string, args: Record<string, unknown>, risk: 'low' | 'medium' | 'high', description?: string): Promise<ApprovalResult>;
|
|
63
|
+
private processBatch;
|
|
64
|
+
private isBlacklisted;
|
|
65
|
+
private isWhitelisted;
|
|
66
|
+
private getPattern;
|
|
67
|
+
private getAlwaysKey;
|
|
68
|
+
private readline;
|
|
69
|
+
close(): void;
|
|
70
|
+
addAutoApprove(toolName: string): void;
|
|
71
|
+
reset(): void;
|
|
72
|
+
private recordStats;
|
|
73
|
+
getStats(): ToolApprovalStats[];
|
|
74
|
+
private loadPersistentDecisions;
|
|
75
|
+
private savePersistentDecisions;
|
|
76
|
+
/**
|
|
77
|
+
* Print approval statistics
|
|
78
|
+
*/
|
|
79
|
+
printStats(): void;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=approval.d.ts.map
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - Enhanced Approval System
|
|
3
|
+
// Real interactive approval with diff preview, batch approve,
|
|
4
|
+
// whitelist/blacklist, and multiple permission modes
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { createInterface } from 'readline';
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { DiffPreview } from './diff-preview.js';
|
|
12
|
+
const APPROVALS_PATH = join(homedir(), '.neuro', 'approvals.json');
|
|
13
|
+
export class ApprovalSystem {
|
|
14
|
+
mode = 'manual';
|
|
15
|
+
sessionApprovals = new Map();
|
|
16
|
+
deniedTools = new Set();
|
|
17
|
+
persistentApprovals = new Map();
|
|
18
|
+
whitelist = new Set();
|
|
19
|
+
blacklist = new Set();
|
|
20
|
+
showDiffPreview = true;
|
|
21
|
+
batchApprove = true;
|
|
22
|
+
maxConsecutiveAutoApproves = 0;
|
|
23
|
+
persistDecisions = true;
|
|
24
|
+
consecutiveAutoApproves = 0;
|
|
25
|
+
rl = null;
|
|
26
|
+
stats = new Map();
|
|
27
|
+
pendingBatch = [];
|
|
28
|
+
batchTimer = null;
|
|
29
|
+
autoApprovePatterns = [
|
|
30
|
+
'read_file', 'search_files', 'list_directory', 'web_search', 'web_fetch',
|
|
31
|
+
'recall_memory', 'project_context', 'todowrite',
|
|
32
|
+
];
|
|
33
|
+
constructor(mode, config) {
|
|
34
|
+
if (mode)
|
|
35
|
+
this.mode = mode;
|
|
36
|
+
if (config) {
|
|
37
|
+
if (config.whitelist)
|
|
38
|
+
this.whitelist = new Set(config.whitelist);
|
|
39
|
+
if (config.blacklist)
|
|
40
|
+
this.blacklist = new Set(config.blacklist);
|
|
41
|
+
if (config.showDiffPreview !== undefined)
|
|
42
|
+
this.showDiffPreview = config.showDiffPreview;
|
|
43
|
+
if (config.batchApprove !== undefined)
|
|
44
|
+
this.batchApprove = config.batchApprove;
|
|
45
|
+
if (config.maxConsecutiveAutoApproves !== undefined)
|
|
46
|
+
this.maxConsecutiveAutoApproves = config.maxConsecutiveAutoApproves;
|
|
47
|
+
if (config.persistDecisions !== undefined)
|
|
48
|
+
this.persistDecisions = config.persistDecisions;
|
|
49
|
+
}
|
|
50
|
+
this.loadPersistentDecisions();
|
|
51
|
+
}
|
|
52
|
+
setMode(mode) { this.mode = mode; }
|
|
53
|
+
getMode() { return this.mode; }
|
|
54
|
+
cycleMode() {
|
|
55
|
+
const modes = ['manual', 'auto', 'plan', 'yolo'];
|
|
56
|
+
const idx = modes.indexOf(this.mode);
|
|
57
|
+
this.mode = modes[(idx + 1) % modes.length];
|
|
58
|
+
return this.mode;
|
|
59
|
+
}
|
|
60
|
+
getModeDescription() {
|
|
61
|
+
const descriptions = {
|
|
62
|
+
manual: 'Ask for every action',
|
|
63
|
+
auto: 'Auto-approve safe operations, ask for dangerous ones',
|
|
64
|
+
plan: 'Read-only mode - no modifications allowed',
|
|
65
|
+
yolo: 'Auto-approve everything (dangerous)',
|
|
66
|
+
};
|
|
67
|
+
return descriptions[this.mode];
|
|
68
|
+
}
|
|
69
|
+
getModeIcon() {
|
|
70
|
+
const icons = { manual: '🛡️', auto: '⚡', plan: '📖', yolo: '🔥' };
|
|
71
|
+
return icons[this.mode];
|
|
72
|
+
}
|
|
73
|
+
// --- Whitelist/Blacklist ---
|
|
74
|
+
addToWhitelist(toolName) { this.whitelist.add(toolName); this.savePersistentDecisions(); }
|
|
75
|
+
removeFromWhitelist(toolName) { this.whitelist.delete(toolName); }
|
|
76
|
+
addToBlacklist(toolName) { this.blacklist.add(toolName); this.savePersistentDecisions(); }
|
|
77
|
+
removeFromBlacklist(toolName) { this.blacklist.delete(toolName); }
|
|
78
|
+
getWhitelist() { return Array.from(this.whitelist); }
|
|
79
|
+
getBlacklist() { return Array.from(this.blacklist); }
|
|
80
|
+
// --- Main Approval Logic ---
|
|
81
|
+
async requestApproval(toolName, args, risk, description) {
|
|
82
|
+
const startTime = Date.now();
|
|
83
|
+
// 1. Blacklist check
|
|
84
|
+
if (this.isBlacklisted(toolName)) {
|
|
85
|
+
this.recordStats(toolName, false, Date.now() - startTime);
|
|
86
|
+
return { approved: false, remember: false };
|
|
87
|
+
}
|
|
88
|
+
// 2. Whitelist check
|
|
89
|
+
if (this.isWhitelisted(toolName)) {
|
|
90
|
+
this.recordStats(toolName, true, Date.now() - startTime);
|
|
91
|
+
return { approved: true, remember: true };
|
|
92
|
+
}
|
|
93
|
+
// 3. Persistent "always" decisions
|
|
94
|
+
const alwaysKey = this.getAlwaysKey(toolName, args);
|
|
95
|
+
if (this.persistentApprovals.has(alwaysKey)) {
|
|
96
|
+
const approved = this.persistentApprovals.get(alwaysKey);
|
|
97
|
+
this.recordStats(toolName, approved, Date.now() - startTime);
|
|
98
|
+
return { approved, remember: true, always: true };
|
|
99
|
+
}
|
|
100
|
+
// 4. Session-level approvals
|
|
101
|
+
const pattern = `${toolName}:${this.getPattern(args)}`;
|
|
102
|
+
if (this.sessionApprovals.has(pattern)) {
|
|
103
|
+
const approved = this.sessionApprovals.get(pattern);
|
|
104
|
+
this.recordStats(toolName, approved, Date.now() - startTime);
|
|
105
|
+
if (approved && this.maxConsecutiveAutoApproves > 0) {
|
|
106
|
+
this.consecutiveAutoApproves++;
|
|
107
|
+
if (this.consecutiveAutoApproves > this.maxConsecutiveAutoApproves) {
|
|
108
|
+
this.consecutiveAutoApproves = 0;
|
|
109
|
+
return this.promptUser(toolName, args, risk, description);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { approved, remember: true };
|
|
113
|
+
}
|
|
114
|
+
// 5. Plan mode
|
|
115
|
+
if (this.mode === 'plan') {
|
|
116
|
+
const isReadOnly = this.autoApprovePatterns.includes(toolName) || this.isWhitelisted(toolName);
|
|
117
|
+
this.recordStats(toolName, isReadOnly, Date.now() - startTime);
|
|
118
|
+
return { approved: isReadOnly, remember: false };
|
|
119
|
+
}
|
|
120
|
+
// 6. Yolo mode
|
|
121
|
+
if (this.mode === 'yolo') {
|
|
122
|
+
this.recordStats(toolName, true, Date.now() - startTime);
|
|
123
|
+
return { approved: true, remember: false };
|
|
124
|
+
}
|
|
125
|
+
// 7. Auto mode
|
|
126
|
+
if (this.mode === 'auto') {
|
|
127
|
+
if (risk === 'low' && (this.autoApprovePatterns.includes(toolName) || this.isWhitelisted(toolName))) {
|
|
128
|
+
this.recordStats(toolName, true, Date.now() - startTime);
|
|
129
|
+
return { approved: true, remember: false };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 8. Known safe tools
|
|
133
|
+
if (risk === 'low' && this.autoApprovePatterns.includes(toolName)) {
|
|
134
|
+
this.recordStats(toolName, true, Date.now() - startTime);
|
|
135
|
+
return { approved: true, remember: false };
|
|
136
|
+
}
|
|
137
|
+
// 9. Diff preview for file modification tools
|
|
138
|
+
if (this.showDiffPreview && this.isFileModification(toolName, args)) {
|
|
139
|
+
return this.promptWithDiffPreview(toolName, args, risk, description);
|
|
140
|
+
}
|
|
141
|
+
// 10. Interactive prompt
|
|
142
|
+
return this.promptUser(toolName, args, risk, description);
|
|
143
|
+
}
|
|
144
|
+
// --- Diff Preview Integration ---
|
|
145
|
+
isFileModification(toolName, args) {
|
|
146
|
+
return ['write_file', 'edit_file', 'apply_diff', 'delete_file'].includes(toolName);
|
|
147
|
+
}
|
|
148
|
+
async promptWithDiffPreview(toolName, args, risk, description) {
|
|
149
|
+
let diff = null;
|
|
150
|
+
try {
|
|
151
|
+
if (toolName === 'edit_file' && args.path && args.old_text && args.new_text) {
|
|
152
|
+
diff = DiffPreview.createEditDiff(args.path, args.old_text, args.new_text);
|
|
153
|
+
}
|
|
154
|
+
else if (toolName === 'write_file' && args.path && args.content) {
|
|
155
|
+
const oldContent = args.old_content || '';
|
|
156
|
+
diff = DiffPreview.createDiff(oldContent, args.content, args.path);
|
|
157
|
+
}
|
|
158
|
+
else if (toolName === 'apply_diff' && args.diff) {
|
|
159
|
+
console.log();
|
|
160
|
+
console.log(chalk.cyan(' ━━━ Diff to Apply ━━━'));
|
|
161
|
+
console.log(chalk.gray(String(args.diff)));
|
|
162
|
+
console.log();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch { /* Diff preview failed, continue without it */ }
|
|
166
|
+
if (diff)
|
|
167
|
+
DiffPreview.renderDiff(diff);
|
|
168
|
+
return this.promptUser(toolName, args, risk, description);
|
|
169
|
+
}
|
|
170
|
+
// --- Interactive Prompt ---
|
|
171
|
+
async promptUser(toolName, args, risk, description) {
|
|
172
|
+
const riskColors = { low: chalk.green, medium: chalk.yellow, high: chalk.red };
|
|
173
|
+
const riskIcons = { low: '🟢', medium: '🟡', high: '🔴' };
|
|
174
|
+
console.log();
|
|
175
|
+
console.log(` ${riskIcons[risk]} ${riskColors[risk].bold(`Approval needed [${risk} risk]`)}`);
|
|
176
|
+
console.log(` ${chalk.cyan('Tool:')} ${chalk.white.bold(toolName)}`);
|
|
177
|
+
if (description) {
|
|
178
|
+
console.log(` ${chalk.cyan('Description:')} ${chalk.gray(description)}`);
|
|
179
|
+
}
|
|
180
|
+
const argEntries = Object.entries(args);
|
|
181
|
+
if (argEntries.length > 0) {
|
|
182
|
+
console.log(` ${chalk.cyan('Arguments:')}`);
|
|
183
|
+
for (const [key, value] of argEntries.slice(0, 5)) {
|
|
184
|
+
const val = typeof value === 'string' && value.length > 80 ? value.slice(0, 80) + '...' : String(value);
|
|
185
|
+
console.log(` ${chalk.gray(key)}: ${chalk.white(val)}`);
|
|
186
|
+
}
|
|
187
|
+
if (argEntries.length > 5) {
|
|
188
|
+
console.log(` ${chalk.gray(`...and ${argEntries.length - 5} more`)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(` ${chalk.gray('y')} = Yes ${chalk.gray('n')} = No ${chalk.gray('a')} = Yes always (session) ${chalk.gray('A')} = Yes always (persist) ${chalk.gray('e')} = Edit args ${chalk.gray('d')} = Show diff`);
|
|
193
|
+
const answer = await this.readline(`${riskIcons[risk]} Allow ${toolName}? [y/n/a/A/e/d]: `);
|
|
194
|
+
const startTime = Date.now();
|
|
195
|
+
switch (answer.toLowerCase().trim()) {
|
|
196
|
+
case 'y':
|
|
197
|
+
case 'yes':
|
|
198
|
+
this.recordStats(toolName, true, Date.now() - startTime);
|
|
199
|
+
this.consecutiveAutoApproves++;
|
|
200
|
+
return { approved: true, remember: false };
|
|
201
|
+
case 'a':
|
|
202
|
+
case 'always': {
|
|
203
|
+
const pattern = `${toolName}:${this.getPattern(args)}`;
|
|
204
|
+
this.sessionApprovals.set(pattern, true);
|
|
205
|
+
this.recordStats(toolName, true, Date.now() - startTime);
|
|
206
|
+
this.consecutiveAutoApproves++;
|
|
207
|
+
return { approved: true, remember: true };
|
|
208
|
+
}
|
|
209
|
+
case 'A': {
|
|
210
|
+
const alwaysKey = this.getAlwaysKey(toolName, args);
|
|
211
|
+
this.persistentApprovals.set(alwaysKey, true);
|
|
212
|
+
this.whitelist.add(toolName);
|
|
213
|
+
if (this.persistDecisions)
|
|
214
|
+
this.savePersistentDecisions();
|
|
215
|
+
this.recordStats(toolName, true, Date.now() - startTime);
|
|
216
|
+
this.consecutiveAutoApproves++;
|
|
217
|
+
return { approved: true, remember: true, always: true };
|
|
218
|
+
}
|
|
219
|
+
case 'e':
|
|
220
|
+
case 'edit':
|
|
221
|
+
console.log(chalk.gray(' Edit mode: modify the arguments and press Enter to approve, or "cancel" to deny'));
|
|
222
|
+
const editedArgs = await this.readline(' New args (JSON): ');
|
|
223
|
+
try {
|
|
224
|
+
JSON.parse(editedArgs);
|
|
225
|
+
this.recordStats(toolName, true, Date.now() - startTime);
|
|
226
|
+
return { approved: true, remember: false, edited: true };
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
this.recordStats(toolName, false, Date.now() - startTime);
|
|
230
|
+
return { approved: false, remember: false };
|
|
231
|
+
}
|
|
232
|
+
case 'd': {
|
|
233
|
+
if (this.isFileModification(toolName, args)) {
|
|
234
|
+
return this.promptWithDiffPreview(toolName, args, risk, description);
|
|
235
|
+
}
|
|
236
|
+
console.log(chalk.gray(' No diff available for this tool.'));
|
|
237
|
+
return this.promptUser(toolName, args, risk, description);
|
|
238
|
+
}
|
|
239
|
+
case 'n':
|
|
240
|
+
case 'no':
|
|
241
|
+
default:
|
|
242
|
+
this.deniedTools.add(toolName);
|
|
243
|
+
this.consecutiveAutoApproves = 0;
|
|
244
|
+
this.recordStats(toolName, false, Date.now() - startTime);
|
|
245
|
+
return { approved: false, remember: false };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// --- Batch Approval ---
|
|
249
|
+
async requestBatchApproval(toolName, args, risk, description) {
|
|
250
|
+
if (!this.batchApprove || this.mode === 'yolo') {
|
|
251
|
+
return this.requestApproval(toolName, args, risk, description);
|
|
252
|
+
}
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
this.pendingBatch.push({ toolName, args, risk, description, resolve });
|
|
255
|
+
if (this.batchTimer)
|
|
256
|
+
clearTimeout(this.batchTimer);
|
|
257
|
+
this.batchTimer = setTimeout(() => { this.processBatch(); }, 500);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async processBatch() {
|
|
261
|
+
const batch = [...this.pendingBatch];
|
|
262
|
+
this.pendingBatch = [];
|
|
263
|
+
this.batchTimer = null;
|
|
264
|
+
if (batch.length === 0)
|
|
265
|
+
return;
|
|
266
|
+
if (batch.length === 1) {
|
|
267
|
+
const item = batch[0];
|
|
268
|
+
const result = await this.requestApproval(item.toolName, item.args, item.risk, item.description);
|
|
269
|
+
item.resolve(result);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const groups = new Map();
|
|
273
|
+
for (const item of batch) {
|
|
274
|
+
if (!groups.has(item.toolName))
|
|
275
|
+
groups.set(item.toolName, []);
|
|
276
|
+
groups.get(item.toolName).push(item);
|
|
277
|
+
}
|
|
278
|
+
console.log();
|
|
279
|
+
console.log(chalk.bold.cyan(` ━━━ Batch Approval (${batch.length} operations) ━━━`));
|
|
280
|
+
for (const [toolName, items] of groups) {
|
|
281
|
+
console.log(` ${chalk.cyan(toolName)}: ${chalk.white(`${items.length} operations`)}`);
|
|
282
|
+
for (const item of items.slice(0, 3)) {
|
|
283
|
+
const preview = item.args.path || item.args.command || item.description || '';
|
|
284
|
+
console.log(` - ${chalk.gray(String(preview).slice(0, 60))}`);
|
|
285
|
+
}
|
|
286
|
+
if (items.length > 3)
|
|
287
|
+
console.log(` ${chalk.gray(`...and ${items.length - 3} more`)}`);
|
|
288
|
+
}
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(` ${chalk.gray('y')} = Approve all ${chalk.gray('n')} = Deny all ${chalk.gray('r')} = Review each`);
|
|
291
|
+
const answer = await this.readline(' Batch approve? [y/n/r]: ');
|
|
292
|
+
switch (answer.toLowerCase().trim()) {
|
|
293
|
+
case 'y':
|
|
294
|
+
case 'yes':
|
|
295
|
+
for (const item of batch) {
|
|
296
|
+
const pattern = `${item.toolName}:${this.getPattern(item.args)}`;
|
|
297
|
+
this.sessionApprovals.set(pattern, true);
|
|
298
|
+
item.resolve({ approved: true, remember: true });
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
case 'r':
|
|
302
|
+
case 'review':
|
|
303
|
+
for (const item of batch) {
|
|
304
|
+
const result = await this.requestApproval(item.toolName, item.args, item.risk, item.description);
|
|
305
|
+
item.resolve(result);
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
default:
|
|
309
|
+
for (const item of batch)
|
|
310
|
+
item.resolve({ approved: false, remember: false });
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// --- Helpers ---
|
|
315
|
+
isBlacklisted(toolName) { return this.blacklist.has(toolName); }
|
|
316
|
+
isWhitelisted(toolName) {
|
|
317
|
+
if (this.whitelist.has(toolName))
|
|
318
|
+
return true;
|
|
319
|
+
for (const pattern of this.whitelist) {
|
|
320
|
+
if (pattern.endsWith('*') && toolName.startsWith(pattern.slice(0, -1)))
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
getPattern(args) {
|
|
326
|
+
if (args.command && typeof args.command === 'string')
|
|
327
|
+
return args.command.split(' ')[0];
|
|
328
|
+
if (args.path && typeof args.path === 'string') {
|
|
329
|
+
return args.path.includes('.') ? '.' + args.path.split('.').pop() : '*';
|
|
330
|
+
}
|
|
331
|
+
return '*';
|
|
332
|
+
}
|
|
333
|
+
getAlwaysKey(toolName, args) {
|
|
334
|
+
return `${toolName}:${this.getPattern(args)}`;
|
|
335
|
+
}
|
|
336
|
+
readline(prompt) {
|
|
337
|
+
if (!this.rl)
|
|
338
|
+
this.rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
339
|
+
return new Promise((resolve) => { this.rl.question(prompt, (answer) => resolve(answer)); });
|
|
340
|
+
}
|
|
341
|
+
close() {
|
|
342
|
+
if (this.rl) {
|
|
343
|
+
this.rl.close();
|
|
344
|
+
this.rl = null;
|
|
345
|
+
}
|
|
346
|
+
if (this.batchTimer) {
|
|
347
|
+
clearTimeout(this.batchTimer);
|
|
348
|
+
this.batchTimer = null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
addAutoApprove(toolName) {
|
|
352
|
+
if (!this.autoApprovePatterns.includes(toolName))
|
|
353
|
+
this.autoApprovePatterns.push(toolName);
|
|
354
|
+
}
|
|
355
|
+
reset() {
|
|
356
|
+
this.sessionApprovals.clear();
|
|
357
|
+
this.deniedTools.clear();
|
|
358
|
+
this.consecutiveAutoApproves = 0;
|
|
359
|
+
}
|
|
360
|
+
// --- Statistics ---
|
|
361
|
+
recordStats(toolName, approved, responseTimeMs) {
|
|
362
|
+
const existing = this.stats.get(toolName) || { approved: 0, denied: 0, totalResponseMs: 0 };
|
|
363
|
+
if (approved)
|
|
364
|
+
existing.approved++;
|
|
365
|
+
else
|
|
366
|
+
existing.denied++;
|
|
367
|
+
existing.totalResponseMs += responseTimeMs;
|
|
368
|
+
this.stats.set(toolName, existing);
|
|
369
|
+
}
|
|
370
|
+
getStats() {
|
|
371
|
+
const result = [];
|
|
372
|
+
for (const [toolName, data] of this.stats) {
|
|
373
|
+
const total = data.approved + data.denied;
|
|
374
|
+
result.push({ toolName, approved: data.approved, denied: data.denied, avgResponseTimeMs: total > 0 ? Math.round(data.totalResponseMs / total) : 0 });
|
|
375
|
+
}
|
|
376
|
+
return result.sort((a, b) => (b.approved + b.denied) - (a.approved + a.denied));
|
|
377
|
+
}
|
|
378
|
+
// --- Persistent Decisions ---
|
|
379
|
+
loadPersistentDecisions() {
|
|
380
|
+
if (!this.persistDecisions)
|
|
381
|
+
return;
|
|
382
|
+
try {
|
|
383
|
+
if (existsSync(APPROVALS_PATH)) {
|
|
384
|
+
const data = JSON.parse(readFileSync(APPROVALS_PATH, 'utf-8'));
|
|
385
|
+
if (data.whitelist)
|
|
386
|
+
this.whitelist = new Set(data.whitelist);
|
|
387
|
+
if (data.blacklist)
|
|
388
|
+
this.blacklist = new Set(data.blacklist);
|
|
389
|
+
if (data.alwaysApprove) {
|
|
390
|
+
for (const [key, val] of Object.entries(data.alwaysApprove)) {
|
|
391
|
+
this.persistentApprovals.set(key, val);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch { /* Ignore errors */ }
|
|
397
|
+
}
|
|
398
|
+
savePersistentDecisions() {
|
|
399
|
+
if (!this.persistDecisions)
|
|
400
|
+
return;
|
|
401
|
+
try {
|
|
402
|
+
const dir = join(APPROVALS_PATH, '..');
|
|
403
|
+
if (!existsSync(dir))
|
|
404
|
+
mkdirSync(dir, { recursive: true });
|
|
405
|
+
const data = {
|
|
406
|
+
whitelist: Array.from(this.whitelist),
|
|
407
|
+
blacklist: Array.from(this.blacklist),
|
|
408
|
+
alwaysApprove: Object.fromEntries(this.persistentApprovals),
|
|
409
|
+
};
|
|
410
|
+
writeFileSync(APPROVALS_PATH, JSON.stringify(data, null, 2), 'utf-8');
|
|
411
|
+
}
|
|
412
|
+
catch { /* Ignore errors */ }
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Print approval statistics
|
|
416
|
+
*/
|
|
417
|
+
printStats() {
|
|
418
|
+
const stats = this.getStats();
|
|
419
|
+
if (stats.length === 0) {
|
|
420
|
+
console.log(chalk.gray(' No approval data yet.'));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
console.log(chalk.bold('\n Approval Statistics:\n'));
|
|
424
|
+
console.log(` ${'Tool'.padEnd(25)} ${'Approved'.padEnd(10)} ${'Denied'.padEnd(10)} ${'Avg Time'.padEnd(10)}`);
|
|
425
|
+
console.log(` ${'─'.repeat(55)}`);
|
|
426
|
+
for (const stat of stats) {
|
|
427
|
+
console.log(` ${chalk.cyan(stat.toolName.padEnd(25))} ${chalk.green(String(stat.approved).padEnd(10))} ${chalk.red(String(stat.denied).padEnd(10))} ${chalk.gray((stat.avgResponseTimeMs + 'ms').padEnd(10))}`);
|
|
428
|
+
}
|
|
429
|
+
console.log();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
//# sourceMappingURL=approval.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Message } from './types.js';
|
|
2
|
+
import { ContextCompactor } from '../context/compaction.js';
|
|
3
|
+
export interface AutoCompactConfig {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
warningThreshold: number;
|
|
6
|
+
compactThreshold: number;
|
|
7
|
+
emergencyThreshold: number;
|
|
8
|
+
preserveRecentCount: number;
|
|
9
|
+
preserveSystemPrompt: boolean;
|
|
10
|
+
compactStrategy: 'summarize' | 'drop-oldest' | 'hybrid';
|
|
11
|
+
tokenBudget: {
|
|
12
|
+
systemPrompt: number;
|
|
13
|
+
conversation: number;
|
|
14
|
+
tools: number;
|
|
15
|
+
output: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface ContextUsage {
|
|
19
|
+
totalTokens: number;
|
|
20
|
+
maxTokens: number;
|
|
21
|
+
usagePercent: number;
|
|
22
|
+
breakdown: {
|
|
23
|
+
systemPrompt: number;
|
|
24
|
+
conversation: number;
|
|
25
|
+
tools: number;
|
|
26
|
+
available: number;
|
|
27
|
+
};
|
|
28
|
+
level: 'normal' | 'warning' | 'compact' | 'emergency';
|
|
29
|
+
}
|
|
30
|
+
export interface AutoCompactResult {
|
|
31
|
+
messages: Message[];
|
|
32
|
+
usage: ContextUsage;
|
|
33
|
+
strategy: AutoCompactConfig['compactStrategy'];
|
|
34
|
+
tokensSaved: number;
|
|
35
|
+
level: ContextUsage['level'];
|
|
36
|
+
}
|
|
37
|
+
export declare const DEFAULT_AUTO_COMPACT_CONFIG: AutoCompactConfig;
|
|
38
|
+
export declare class AutoCompactManager {
|
|
39
|
+
private config;
|
|
40
|
+
private currentModelId;
|
|
41
|
+
private contextManager;
|
|
42
|
+
private compactor;
|
|
43
|
+
private warningCallbacks;
|
|
44
|
+
private compactCallbacks;
|
|
45
|
+
private emergencyCallbacks;
|
|
46
|
+
private lastLevel;
|
|
47
|
+
constructor(config?: Partial<AutoCompactConfig>);
|
|
48
|
+
/**
|
|
49
|
+
* Update the current model and recalculate model-aware thresholds.
|
|
50
|
+
* Rebuilds ContextManager and ContextCompactor for the new model.
|
|
51
|
+
*/
|
|
52
|
+
setModel(modelId: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Set the ContextCompactor for LLM-backed summarization.
|
|
55
|
+
* This is optional — without it, auto-compact uses local strategies only.
|
|
56
|
+
*/
|
|
57
|
+
setCompactor(compactor: ContextCompactor): void;
|
|
58
|
+
/**
|
|
59
|
+
* Compute detailed context usage breakdown for the given messages and model.
|
|
60
|
+
*/
|
|
61
|
+
getContextUsage(messages: Message[], modelId: string): ContextUsage;
|
|
62
|
+
/**
|
|
63
|
+
* Determine whether compaction is needed and at what level.
|
|
64
|
+
* Fires callbacks when the level transitions.
|
|
65
|
+
*/
|
|
66
|
+
shouldCompact(messages: Message[], modelId: string): {
|
|
67
|
+
needed: boolean;
|
|
68
|
+
level: ContextUsage['level'];
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Compact messages according to the configured or specified strategy.
|
|
72
|
+
* Returns the compacted message list along with usage metrics.
|
|
73
|
+
*/
|
|
74
|
+
compact(messages: Message[], strategy?: AutoCompactConfig['compactStrategy']): Promise<Message[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Strategy: Summarize older messages into a single context message.
|
|
77
|
+
* Keeps last N messages verbatim, summarizes everything else.
|
|
78
|
+
*/
|
|
79
|
+
private compactBySummarize;
|
|
80
|
+
/**
|
|
81
|
+
* Strategy: Drop oldest messages (except system prompt and recent).
|
|
82
|
+
*/
|
|
83
|
+
private compactByDropOldest;
|
|
84
|
+
/**
|
|
85
|
+
* Strategy: Hybrid — summarize first, then drop oldest if still over budget.
|
|
86
|
+
*/
|
|
87
|
+
private compactByHybrid;
|
|
88
|
+
/**
|
|
89
|
+
* Emergency compaction: keep only system prompt + last 3 exchanges (6 messages).
|
|
90
|
+
* This is the last resort when everything else fails.
|
|
91
|
+
*/
|
|
92
|
+
private emergencyDrop;
|
|
93
|
+
/**
|
|
94
|
+
* Estimate the number of tokens in a text string for a given model.
|
|
95
|
+
* Uses ~4 chars/token for English, ~2 chars/token for CJK.
|
|
96
|
+
*/
|
|
97
|
+
estimateTokens(text: string, _modelId?: string): number;
|
|
98
|
+
/**
|
|
99
|
+
* Estimate total tokens across an array of messages,
|
|
100
|
+
* including tool calls.
|
|
101
|
+
*/
|
|
102
|
+
estimateMessagesTokens(messages: Message[]): number;
|
|
103
|
+
/**
|
|
104
|
+
* Estimate tokens for a single message including tool calls.
|
|
105
|
+
*/
|
|
106
|
+
private estimateMessageTokens;
|
|
107
|
+
onWarning(callback: (usage: ContextUsage) => void): void;
|
|
108
|
+
onCompact(callback: (usage: ContextUsage, result: Message[]) => void): void;
|
|
109
|
+
onEmergency(callback: (usage: ContextUsage) => void): void;
|
|
110
|
+
/**
|
|
111
|
+
* Partition messages into system and non-system buckets.
|
|
112
|
+
*/
|
|
113
|
+
private partitionMessages;
|
|
114
|
+
/**
|
|
115
|
+
* Build a concise summary from a list of messages.
|
|
116
|
+
* Prioritizes user intent, tool interactions, and assistant decisions.
|
|
117
|
+
*/
|
|
118
|
+
private buildSummary;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Middleware function that can be called before each LLM call.
|
|
122
|
+
* Checks context usage, auto-compacts if needed, and returns
|
|
123
|
+
* the (possibly compacted) message list.
|
|
124
|
+
*/
|
|
125
|
+
export declare function autoCompactMiddleware(manager: AutoCompactManager, messages: Message[], modelId: string): Promise<Message[]>;
|
|
126
|
+
export declare function createAutoCompactManager(overrides?: Partial<AutoCompactConfig>): AutoCompactManager;
|
|
127
|
+
//# sourceMappingURL=auto-compact.d.ts.map
|