@bahulam/code 2.6.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/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval Flow — permission prompts for tool execution.
|
|
3
|
+
*
|
|
4
|
+
* Modeled after Claude Code's visual pattern:
|
|
5
|
+
* ⏺ Tool(args) ← tool header
|
|
6
|
+
* ⎿ Allow? [y/n/a/t] ← inline prompt
|
|
7
|
+
* ⎿ ✓ allowed ← result
|
|
8
|
+
*
|
|
9
|
+
* Write tools require approval. Read tools auto-approve.
|
|
10
|
+
* Shell commands are risk-assessed (safe/medium/high).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { shellCommandDisplay, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
14
|
+
import {
|
|
15
|
+
classify as classifyTier,
|
|
16
|
+
TIERS,
|
|
17
|
+
requiresExplicitApproval,
|
|
18
|
+
requiresCheckpoint,
|
|
19
|
+
label as tierLabel,
|
|
20
|
+
} from './risk-tier.mjs';
|
|
21
|
+
import {
|
|
22
|
+
renderApprovalPrompt,
|
|
23
|
+
renderTrustedApproval,
|
|
24
|
+
defaultOptions as approvalOptions,
|
|
25
|
+
} from '../ui/approval.mjs';
|
|
26
|
+
import { isInputDockMounted, moveToContent } from '../ui/input-dock.mjs';
|
|
27
|
+
import { validateShellCommand } from './safety.mjs';
|
|
28
|
+
import { classifyCommand } from '../permissions/command-classifier.mjs';
|
|
29
|
+
import { ApprovalLog } from './approval-log.mjs';
|
|
30
|
+
import { TrustStore } from './trust.mjs';
|
|
31
|
+
import { loadEffectivePolicy } from './policy-resolver.mjs';
|
|
32
|
+
|
|
33
|
+
// ── Tool Classification ──
|
|
34
|
+
//
|
|
35
|
+
// Risk tiering moved to src/core/risk-tier.mjs (PRD-055 §8.1). WRITE_TOOLS
|
|
36
|
+
// stays here only because `planMode` blocks anything that writes.
|
|
37
|
+
|
|
38
|
+
const WRITE_TOOLS = new Set([
|
|
39
|
+
'shell', 'write_file', 'write_project', 'edit_file', 'delete_file',
|
|
40
|
+
'validate_build', 'lint_check',
|
|
41
|
+
'skill_install', 'skill_update', 'skill_remove',
|
|
42
|
+
'agent_create', 'agent_sync',
|
|
43
|
+
'workflow_create_multi', 'workflow_sync_multi',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function defaultWhy(tier, tool, args) {
|
|
47
|
+
const subject = approvalSummary(tool, args);
|
|
48
|
+
switch (tier) {
|
|
49
|
+
case TIERS.SENSITIVE_READ:
|
|
50
|
+
return `Reads a sensitive path (${subject || 'secret-like file'}). Confirm before exposing its contents to the agent.`;
|
|
51
|
+
case TIERS.SHELL_DANGEROUS:
|
|
52
|
+
return `Shell command matches a high-risk pattern (rm -rf, sudo, force push, etc.). Confirm before running.`;
|
|
53
|
+
case TIERS.DESTRUCTIVE:
|
|
54
|
+
return `${tool} permanently mutates project state. Confirm before running.`;
|
|
55
|
+
case TIERS.SHELL_MEDIUM:
|
|
56
|
+
return `Mutates the workspace or environment (install, build, commit, push).`;
|
|
57
|
+
case TIERS.NETWORK:
|
|
58
|
+
return `Reaches an external network endpoint.`;
|
|
59
|
+
default:
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function approvalSummary(tool, args = {}) {
|
|
65
|
+
const summary = toolDisplaySummary(tool, args);
|
|
66
|
+
if (tool !== 'shell') return summary;
|
|
67
|
+
const display = shellCommandDisplay(args.command || args.cmd || summary || '');
|
|
68
|
+
return display.cwdLabel ? `${display.command} in ${display.cwdLabel}` : display.command;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function shellHardBlockReason(tool, args = {}) {
|
|
72
|
+
if (tool !== 'shell') return '';
|
|
73
|
+
const command = args.command || args.cmd || '';
|
|
74
|
+
const safety = validateShellCommand(command);
|
|
75
|
+
if (!safety.safe) return safety.reason || 'Blocked by shell safety policy';
|
|
76
|
+
const classification = classifyCommand(command);
|
|
77
|
+
if (classification.classification === 'blocked') return classification.reason || 'Blocked by shell safety policy';
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── ANSI helpers ──
|
|
82
|
+
|
|
83
|
+
const RST = '\x1b[0m';
|
|
84
|
+
const DIM = '\x1b[2m';
|
|
85
|
+
const BOLD = '\x1b[1m';
|
|
86
|
+
const CYAN = '\x1b[36m';
|
|
87
|
+
const GREEN = '\x1b[32m';
|
|
88
|
+
const RED = '\x1b[31m';
|
|
89
|
+
const YELLOW = '\x1b[33m';
|
|
90
|
+
const GRAY = '\x1b[90m';
|
|
91
|
+
const WHITE = '\x1b[37m';
|
|
92
|
+
|
|
93
|
+
const write = (s) => process.stderr.write(s);
|
|
94
|
+
|
|
95
|
+
// ── Approval Manager ──
|
|
96
|
+
|
|
97
|
+
export class ApprovalManager {
|
|
98
|
+
constructor({ autoApprove = false, planMode = false, cwd = process.cwd(), policy = null, trustStore = null, approvalLog = null } = {}) {
|
|
99
|
+
this.autoApprove = autoApprove;
|
|
100
|
+
this.planMode = planMode;
|
|
101
|
+
this.cwd = cwd;
|
|
102
|
+
this.policy = policy || loadEffectivePolicy({ cwd }).policy;
|
|
103
|
+
this.trustStore = trustStore || new TrustStore({ cwd, policy: this.policy });
|
|
104
|
+
this.approvalLog = approvalLog || new ApprovalLog({ cwd });
|
|
105
|
+
this.approveAll = false;
|
|
106
|
+
this.approvedToolTypes = new Set();
|
|
107
|
+
this.history = [];
|
|
108
|
+
this.rejectionHints = [];
|
|
109
|
+
this._rl = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setReadline(rl) { this._rl = rl; }
|
|
113
|
+
|
|
114
|
+
setExecutionHooks({ onPause, onResume } = {}) {
|
|
115
|
+
this._execPause = onPause || null;
|
|
116
|
+
this._execResume = onResume || null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
revoke() {
|
|
120
|
+
const wasActive = this.approveAll || this.approvedToolTypes.size > 0;
|
|
121
|
+
this.approveAll = false;
|
|
122
|
+
this.approvedToolTypes.clear();
|
|
123
|
+
const trustActive = this.trustStore?.revoke?.() || false;
|
|
124
|
+
return wasActive || trustActive;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getModeLabel() {
|
|
128
|
+
if (this.approveAll) return `${GREEN}allow-all${RST}`;
|
|
129
|
+
if (this.approvedToolTypes.size > 0) {
|
|
130
|
+
return `${CYAN}auto: ${[...this.approvedToolTypes].join(', ')}${RST}`;
|
|
131
|
+
}
|
|
132
|
+
return `${DIM}ask${RST}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async check(toolName, args, requireApproval = false, context = {}) {
|
|
136
|
+
if (this.planMode && WRITE_TOOLS.has(toolName)) {
|
|
137
|
+
return { approved: false, reason: `Blocked by plan mode: ${toolName}` };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const tier = classifyTier(toolName, args);
|
|
141
|
+
const hardBlock = shellHardBlockReason(toolName, args);
|
|
142
|
+
if (hardBlock) {
|
|
143
|
+
const reason = `Blocked by safety policy: ${hardBlock}`;
|
|
144
|
+
this.history.push({ tool: toolName, decision: 'safety-block', tier, time: Date.now(), reason });
|
|
145
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'safety_block', scope: 'none', reason });
|
|
146
|
+
write(` ${RED}✗${RST} ${DIM}${reason}${RST}\n\n`);
|
|
147
|
+
return { approved: false, tier, reason, blocked: true, code: 'safety_block' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Auto-approve everything in headless/autoApprove mode (no TTY prompts).
|
|
151
|
+
// Non-overridable shell safety blocks are checked above.
|
|
152
|
+
if (this.autoApprove) {
|
|
153
|
+
this.history.push({ tool: toolName, decision: 'auto', tier, time: Date.now() });
|
|
154
|
+
return { approved: true, tier };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 'auto' tiers: read, shell-safe.
|
|
158
|
+
if (tier === TIERS.READ || tier === TIERS.SHELL_SAFE) {
|
|
159
|
+
this.history.push({ tool: toolName, decision: 'auto-tier', tier, time: Date.now() });
|
|
160
|
+
return { approved: true, tier };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 'auto-with-undo' tier: local-edit. Checkpoint is taken by the tool
|
|
164
|
+
// executor before the edit; here we just approve.
|
|
165
|
+
if (tier === TIERS.LOCAL_EDIT) {
|
|
166
|
+
this.history.push({ tool: toolName, decision: 'auto-with-undo', tier, time: Date.now() });
|
|
167
|
+
return { approved: true, tier, requireCheckpoint: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const trust = this.trustStore?.find?.(toolName, args, tier);
|
|
171
|
+
if (trust?.decision === 'deny') {
|
|
172
|
+
this.history.push({ tool: toolName, decision: 'trusted-deny', tier, time: Date.now(), rule_id: trust.rule?.id });
|
|
173
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'deny_trusted', scope: trust.rule?.scope, rule_id: trust.rule?.id });
|
|
174
|
+
return { approved: false, tier, reason: `Denied by trust rule ${trust.rule?.id || ''}`.trim() };
|
|
175
|
+
}
|
|
176
|
+
if (trust?.decision === 'allow') {
|
|
177
|
+
this.history.push({ tool: toolName, decision: 'auto_trusted', tier, time: Date.now(), rule_id: trust.rule?.id });
|
|
178
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'auto_trusted', scope: trust.rule?.scope, rule_id: trust.rule?.id });
|
|
179
|
+
write(renderTrustedApproval({ tool: toolName, args, scope: trust.rule?.scope, ruleId: trust.rule?.id }));
|
|
180
|
+
return { approved: true, tier, scope: trust.rule?.scope, rule_id: trust.rule?.id };
|
|
181
|
+
}
|
|
182
|
+
if (trust?.decision === 'reask' && trust.reason) {
|
|
183
|
+
context.reason = context.reason || context.why || `Re-asking: ${trust.reason}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Honor approve-all / type-allow shortcuts for non-explicit tiers only.
|
|
187
|
+
if (!requiresExplicitApproval(tier)) {
|
|
188
|
+
if (this.approveAll) {
|
|
189
|
+
this.history.push({ tool: toolName, decision: 'auto-all', tier, time: Date.now() });
|
|
190
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'auto-all', scope: 'session' });
|
|
191
|
+
return { approved: true, tier };
|
|
192
|
+
}
|
|
193
|
+
if (this.approvedToolTypes.has(toolName)) {
|
|
194
|
+
this.history.push({ tool: toolName, decision: 'type-auto', tier, time: Date.now() });
|
|
195
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-auto', scope: 'session' });
|
|
196
|
+
return { approved: true, tier };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return this._prompt(toolName, args, { ...context, tier });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async _prompt(toolName, args, context = {}) {
|
|
204
|
+
const tier = context.tier || classifyTier(toolName, args);
|
|
205
|
+
const why = context.reason || context.why || defaultWhy(tier, toolName, args);
|
|
206
|
+
const options = this._optionsFor(tier);
|
|
207
|
+
|
|
208
|
+
let selected = 0; // arrow-driven cursor
|
|
209
|
+
let printedHeight = 0;
|
|
210
|
+
|
|
211
|
+
// Approval must render ABOVE the fixed input dock (in the scrollable
|
|
212
|
+
// content area) so it doesn't collide with the "+ add instruction"
|
|
213
|
+
// prompt. Move cursor into content once; the in-place redraw below
|
|
214
|
+
// stays within that region because printedHeight tracks the anchor.
|
|
215
|
+
if (isInputDockMounted()) moveToContent();
|
|
216
|
+
|
|
217
|
+
// For TTYs we redraw in place on every arrow key so the prompt feels
|
|
218
|
+
// live. For non-TTYs / pipes we just print once and read a line.
|
|
219
|
+
const isInteractive = process.stdin.isTTY;
|
|
220
|
+
if (!isInteractive) {
|
|
221
|
+
write(renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options }) + '\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const drawPrompt = () => {
|
|
225
|
+
// Move up over the previous render before re-printing. Use a
|
|
226
|
+
// bounded per-line clear instead of \x1b[J — a full clear-to-end
|
|
227
|
+
// would wipe the input dock's bottom rule and tips row that live
|
|
228
|
+
// below this render position.
|
|
229
|
+
if (printedHeight > 0) {
|
|
230
|
+
write(`\x1b[${printedHeight}F`); // cursor to start of N lines above
|
|
231
|
+
for (let i = 0; i < printedHeight; i++) {
|
|
232
|
+
write('\x1b[2K'); // clear current line
|
|
233
|
+
write('\x1b[1E'); // to start of next line
|
|
234
|
+
}
|
|
235
|
+
write(`\x1b[${printedHeight}F`); // back to the anchor row
|
|
236
|
+
}
|
|
237
|
+
const block = renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options });
|
|
238
|
+
write(block + '\n');
|
|
239
|
+
printedHeight = block.split('\n').length;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (isInteractive) drawPrompt();
|
|
243
|
+
|
|
244
|
+
// ── Input loop ─────────────────────────────────────────────────
|
|
245
|
+
const choose = async () => {
|
|
246
|
+
for (;;) {
|
|
247
|
+
const k = await this._readKey();
|
|
248
|
+
|
|
249
|
+
if (k === 'up' || k === 'left') {
|
|
250
|
+
if (!isInteractive) continue;
|
|
251
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
252
|
+
drawPrompt();
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (k === 'down' || k === 'right' || k === 'tab') {
|
|
256
|
+
if (!isInteractive) continue;
|
|
257
|
+
selected = (selected + 1) % options.length;
|
|
258
|
+
drawPrompt();
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (k === 'return') {
|
|
262
|
+
return options[selected].value;
|
|
263
|
+
}
|
|
264
|
+
if (k === 'escape') return 'reject';
|
|
265
|
+
|
|
266
|
+
// Letter shortcut: match against option.key
|
|
267
|
+
if (typeof k === 'string' && k.length === 1) {
|
|
268
|
+
const lower = k.toLowerCase();
|
|
269
|
+
const idx = options.findIndex(o => o.key === lower);
|
|
270
|
+
if (idx >= 0) {
|
|
271
|
+
selected = idx;
|
|
272
|
+
if (isInteractive) drawPrompt();
|
|
273
|
+
return options[idx].value;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Anything else: ignore and re-read.
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const value = await choose();
|
|
281
|
+
|
|
282
|
+
switch (value) {
|
|
283
|
+
case 'approve':
|
|
284
|
+
this.history.push({ tool: toolName, decision: 'yes', tier, time: Date.now() });
|
|
285
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once' });
|
|
286
|
+
return { approved: true, tier };
|
|
287
|
+
|
|
288
|
+
case 'allow-session': {
|
|
289
|
+
if (!this.policy.hitl?.allowSessionTrust) return this._prompt(toolName, args, context);
|
|
290
|
+
const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'SESSION' });
|
|
291
|
+
write(` ${GREEN}✓${RST} ${DIM}trusted for this session: ${rule.pattern}${RST}\n\n`);
|
|
292
|
+
this.history.push({ tool: toolName, decision: 'session-trust', tier, time: Date.now(), rule_id: rule.id });
|
|
293
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'SESSION', rule_id: rule.id });
|
|
294
|
+
return { approved: true, tier, scope: 'SESSION', rule_id: rule.id };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case 'allow-project': {
|
|
298
|
+
if (!this.policy.hitl?.allowProjectTrust) return this._prompt(toolName, args, context);
|
|
299
|
+
const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'PROJECT' });
|
|
300
|
+
write(` ${GREEN}✓${RST} ${DIM}trusted for this project: ${rule.pattern}${RST}\n\n`);
|
|
301
|
+
this.history.push({ tool: toolName, decision: 'project-trust', tier, time: Date.now(), rule_id: rule.id });
|
|
302
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'PROJECT', rule_id: rule.id });
|
|
303
|
+
return { approved: true, tier, scope: 'PROJECT', rule_id: rule.id };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case 'reject':
|
|
307
|
+
{
|
|
308
|
+
const reason = 'User stopped the command';
|
|
309
|
+
write(` ${RED}✗${RST} ${DIM}stopped${RST}\n\n`);
|
|
310
|
+
this.history.push({ tool: toolName, decision: 'no', tier, time: Date.now(), reason });
|
|
311
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'reject', scope: 'once', reason });
|
|
312
|
+
this._rememberRejection({ tool: toolName, args, tier, decision: 'reject', reason, note: '' });
|
|
313
|
+
return { approved: false, tier, reason };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'allow-all':
|
|
317
|
+
if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
|
|
318
|
+
this.approveAll = true;
|
|
319
|
+
write(` ${GREEN}✓✓${RST} ${DIM}allow-all activated${RST}\n\n`);
|
|
320
|
+
this.history.push({ tool: toolName, decision: 'approve-all', tier, time: Date.now() });
|
|
321
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve-all', scope: 'session' });
|
|
322
|
+
return { approved: true, tier };
|
|
323
|
+
|
|
324
|
+
case 'allow-type':
|
|
325
|
+
if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
|
|
326
|
+
this.approvedToolTypes.add(toolName);
|
|
327
|
+
this.history.push({ tool: toolName, decision: 'type-approve', tier, time: Date.now() });
|
|
328
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session' });
|
|
329
|
+
return { approved: true, tier };
|
|
330
|
+
|
|
331
|
+
case 'why':
|
|
332
|
+
write(`\n ${DIM}${(context.reason || why).slice(0, 400)}${RST}\n\n`);
|
|
333
|
+
printedHeight = 0;
|
|
334
|
+
return this._prompt(toolName, args, context);
|
|
335
|
+
|
|
336
|
+
case 'edit':
|
|
337
|
+
case 'replan':
|
|
338
|
+
{
|
|
339
|
+
const note = await this._readLinePrompt(` ${DIM}How would you like to proceed? ${RST}`);
|
|
340
|
+
const reason = note ? `User asked to re-plan: ${note}` : 'User asked to re-plan';
|
|
341
|
+
write(` ${YELLOW}↩${RST} ${DIM}${note ? `re-plan — ${truncateNote(note)}` : 'reject with hint — rework the plan'}${RST}\n\n`);
|
|
342
|
+
this.history.push({ tool: toolName, decision: 'replan', tier, time: Date.now(), reason });
|
|
343
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'replan', scope: 'once', reason });
|
|
344
|
+
this._rememberRejection({ tool: toolName, args, tier, decision: 'replan', reason, note });
|
|
345
|
+
return { approved: false, tier, reason };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
default:
|
|
349
|
+
return this._prompt(toolName, args, context);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_readKey() {
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
if (!process.stdin.isTTY) {
|
|
356
|
+
resolve('return');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (this._execPause) this._execPause();
|
|
361
|
+
if (this._rl) this._rl.pause();
|
|
362
|
+
|
|
363
|
+
const wasRaw = process.stdin.isRaw;
|
|
364
|
+
process.stdin.setRawMode(true);
|
|
365
|
+
process.stdin.resume();
|
|
366
|
+
process.stdin.once('data', (data) => {
|
|
367
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
368
|
+
if (this._rl) this._rl.resume();
|
|
369
|
+
if (this._execResume) this._execResume();
|
|
370
|
+
|
|
371
|
+
const bytes = [...data];
|
|
372
|
+
const str = data.toString();
|
|
373
|
+
|
|
374
|
+
if (bytes[0] === 0x03) process.exit(0);
|
|
375
|
+
// Arrow keys: ESC [ A/B/C/D (3-byte CSI sequences)
|
|
376
|
+
if (bytes.length === 3 && bytes[0] === 0x1b && bytes[1] === 0x5b) {
|
|
377
|
+
if (bytes[2] === 0x41) { resolve('up'); return; }
|
|
378
|
+
if (bytes[2] === 0x42) { resolve('down'); return; }
|
|
379
|
+
if (bytes[2] === 0x43) { resolve('right'); return; }
|
|
380
|
+
if (bytes[2] === 0x44) { resolve('left'); return; }
|
|
381
|
+
}
|
|
382
|
+
// Bare Esc (single byte) — explicit reject signal
|
|
383
|
+
if (bytes.length === 1 && bytes[0] === 0x1b) { resolve('escape'); return; }
|
|
384
|
+
if (bytes[0] === 0x09) { resolve('tab'); return; }
|
|
385
|
+
if (str === '\r' || str === '\n') { resolve('return'); return; }
|
|
386
|
+
resolve(str);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
_readLinePrompt(label) {
|
|
392
|
+
return new Promise((resolve) => {
|
|
393
|
+
if (!process.stdin.isTTY) {
|
|
394
|
+
resolve('');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (this._execPause) this._execPause();
|
|
399
|
+
if (this._rl) this._rl.pause();
|
|
400
|
+
|
|
401
|
+
const wasRaw = process.stdin.isRaw;
|
|
402
|
+
if (typeof process.stdin.setRawMode === 'function') {
|
|
403
|
+
process.stdin.setRawMode(false);
|
|
404
|
+
}
|
|
405
|
+
process.stdin.resume();
|
|
406
|
+
write(label);
|
|
407
|
+
|
|
408
|
+
let buffer = '';
|
|
409
|
+
const cleanup = () => {
|
|
410
|
+
process.stdin.off('data', onData);
|
|
411
|
+
if (typeof process.stdin.setRawMode === 'function') {
|
|
412
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
413
|
+
}
|
|
414
|
+
if (this._rl) this._rl.resume();
|
|
415
|
+
if (this._execResume) this._execResume();
|
|
416
|
+
};
|
|
417
|
+
const finish = () => {
|
|
418
|
+
cleanup();
|
|
419
|
+
resolve(buffer.trim());
|
|
420
|
+
};
|
|
421
|
+
const onData = (data) => {
|
|
422
|
+
const str = data.toString();
|
|
423
|
+
if (data[0] === 0x03) process.exit(0);
|
|
424
|
+
if (data[0] === 0x1b) {
|
|
425
|
+
buffer = '';
|
|
426
|
+
write('\n');
|
|
427
|
+
finish();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (str.includes('\n') || str.includes('\r')) {
|
|
431
|
+
buffer += str.replace(/[\r\n].*$/s, '');
|
|
432
|
+
finish();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
buffer += str;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
process.stdin.on('data', onData);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
_optionsFor(tier) {
|
|
443
|
+
return approvalOptions(tier);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
getSummary() {
|
|
447
|
+
const approved = this.history.filter(h => h.decision !== 'no').length;
|
|
448
|
+
const denied = this.history.filter(h => h.decision === 'no').length;
|
|
449
|
+
return {
|
|
450
|
+
total: this.history.length,
|
|
451
|
+
approved,
|
|
452
|
+
denied,
|
|
453
|
+
autoApproveAll: this.approveAll,
|
|
454
|
+
autoApprovedTypes: [...this.approvedToolTypes],
|
|
455
|
+
trust: this.trustStore?.summary?.() || { sessionRules: 0, projectRules: 0 },
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_rememberRejection(entry) {
|
|
460
|
+
this.rejectionHints.push({ ...entry, time: Date.now() });
|
|
461
|
+
if (this.rejectionHints.length > 10) {
|
|
462
|
+
this.rejectionHints.splice(0, this.rejectionHints.length - 10);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
consumeRejectionHints() {
|
|
467
|
+
const hints = [...this.rejectionHints];
|
|
468
|
+
this.rejectionHints = [];
|
|
469
|
+
return hints;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function truncateNote(note) {
|
|
474
|
+
const text = String(note || '').trim();
|
|
475
|
+
return text.length <= 120 ? text : text.slice(0, 119) + '…';
|
|
476
|
+
}
|