@bahulam/code 0.1.1
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 +93 -0
- package/package.json +56 -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 +223 -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 +312 -0
- package/src/commands/agent.mjs +221 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +202 -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/model-catalog.mjs +57 -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/prose-chunker.mjs +255 -0
- package/src/context/retriever.mjs +425 -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 +145 -0
- package/src/core/approval.mjs +700 -0
- package/src/core/attachments.mjs +666 -0
- package/src/core/backend-url.mjs +68 -0
- package/src/core/bundled-runtime.mjs +418 -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 +331 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +460 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +538 -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 +388 -0
- package/src/core/safety.mjs +260 -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 +957 -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 +2231 -0
- package/src/core/trust.mjs +160 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -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 +374 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +700 -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 +122 -0
- package/src/terminal/agents.mjs +353 -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 +310 -0
- package/src/terminal/repl-ask-form.mjs +120 -0
- package/src/terminal/repl-explore.mjs +44 -0
- package/src/terminal/repl-format.mjs +317 -0
- package/src/terminal/repl-model-form.mjs +132 -0
- package/src/terminal/repl-render.mjs +833 -0
- package/src/terminal/repl-resume.mjs +640 -0
- package/src/terminal/repl-state.mjs +120 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +5032 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +392 -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 +703 -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 +510 -0
- package/src/ui/banner.mjs +232 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +170 -0
- package/src/ui/input-dock.mjs +772 -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/render-queue.mjs +500 -0
- package/src/ui/slash-commands.mjs +257 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +167 -0
- package/src/ui/term.mjs +174 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +740 -0
- package/src/ui/tool-details.mjs +504 -0
- package/src/ui/transcript-block.mjs +20 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval prompt UI — Mission Control (PRD-055 §8.2).
|
|
3
|
+
*
|
|
4
|
+
* ── ⚠ APPROVAL · SHELL-MEDIUM · shell ───────────────────────────────
|
|
5
|
+
* ⚙️ Running git add package.json && git status --short
|
|
6
|
+
* Decision
|
|
7
|
+
* ▸ [y] approve once run this call
|
|
8
|
+
* [t] always allow auto-approve future calls to this tool
|
|
9
|
+
* [n] cancel do not run
|
|
10
|
+
* ↑↓ move · Enter pick · letter shortcut · Esc cancel
|
|
11
|
+
*
|
|
12
|
+
* The rule colour is `brand.accent` (magenta) for explicit-approval
|
|
13
|
+
* tiers; safe-default prompts use `brand.data` so they read as advisory.
|
|
14
|
+
*
|
|
15
|
+
* Pure — caller writes the returned string to stderr.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { paint, width as visibleWidth } from './palette.mjs';
|
|
19
|
+
import { icon } from './icons.mjs';
|
|
20
|
+
import { shellCommandDisplay, shellCommandProfile, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
21
|
+
import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
|
|
22
|
+
import { isSensitiveConfigPath } from '../core/safety.mjs';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default option set per tier. Caller can override via `opts.options`.
|
|
26
|
+
*
|
|
27
|
+
* Each option is `{ key, label, value, hint? }`:
|
|
28
|
+
* key — single-letter shortcut (case-insensitive)
|
|
29
|
+
* label — text shown in the menu
|
|
30
|
+
* value — return value from the menu loop
|
|
31
|
+
* hint — secondary description shown to the right of the label
|
|
32
|
+
*/
|
|
33
|
+
export function defaultOptions(tier, { tool = '', args = {} } = {}) {
|
|
34
|
+
const approve = { key: 'y', label: 'approve once', value: 'approve', hint: 'run this call' };
|
|
35
|
+
const cancel = { key: 'n', label: 'cancel', value: 'reject', hint: 'do not run' };
|
|
36
|
+
if (requiresExplicitApproval(tier)) {
|
|
37
|
+
return [approve, cancel];
|
|
38
|
+
}
|
|
39
|
+
if (tool === 'shell') {
|
|
40
|
+
return [
|
|
41
|
+
approve,
|
|
42
|
+
{ key: 't', label: 'allow similar', value: 'allow-session', hint: `auto-approve ${shellTrustHint(args)} this session` },
|
|
43
|
+
cancel,
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
approve,
|
|
48
|
+
{ key: 't', label: 'always allow', value: 'allow-type', hint: 'auto-approve future calls to this tool' },
|
|
49
|
+
cancel,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render the compact horizontal prompt with arrow-navigable options.
|
|
55
|
+
*
|
|
56
|
+
* ── ⚠ APPROVAL · SHELL-MEDIUM · shell ───────────────────────────────
|
|
57
|
+
* ⚙️ Running rm -rf node_modules && npm install
|
|
58
|
+
* Decision
|
|
59
|
+
* ▸ [y] approve once
|
|
60
|
+
* [t] always allow
|
|
61
|
+
* [n] cancel
|
|
62
|
+
* ↑↓ move · Enter pick · letter shortcut · Esc cancel
|
|
63
|
+
* ────────────────────────────────────────────────────────────────────
|
|
64
|
+
*
|
|
65
|
+
* @param {object} opts
|
|
66
|
+
* @param {string} opts.tool
|
|
67
|
+
* @param {object} opts.args
|
|
68
|
+
* @param {string} opts.tier
|
|
69
|
+
* @param {string} [opts.why]
|
|
70
|
+
* @param {number} [opts.width]
|
|
71
|
+
* @param {Array} [opts.options] — override default option set
|
|
72
|
+
* @param {number} [opts.selected] — index of the highlighted option
|
|
73
|
+
*/
|
|
74
|
+
export function renderApprovalPrompt({
|
|
75
|
+
tool, args = {}, tier, why = '', width,
|
|
76
|
+
options, selected = 0, showDetails = false,
|
|
77
|
+
} = {}) {
|
|
78
|
+
const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
|
|
79
|
+
const explicit = requiresExplicitApproval(tier);
|
|
80
|
+
const accent = explicit ? paint.brand.accent : paint.brand.data;
|
|
81
|
+
const opts = options || defaultOptions(tier, { tool, args });
|
|
82
|
+
const title = `⚠ ${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`;
|
|
83
|
+
|
|
84
|
+
const lines = [
|
|
85
|
+
blockHeader(title, accent),
|
|
86
|
+
...subjectRows(tool, args, cols, accent),
|
|
87
|
+
...detailRows(tool, args, cols, accent, showDetails),
|
|
88
|
+
...riskRows(tool, args, tier, accent),
|
|
89
|
+
...reasonRows(tool, args, why, cols, accent),
|
|
90
|
+
blockLine(accent),
|
|
91
|
+
...decisionRows(opts, selected, accent),
|
|
92
|
+
blockLine(accent, paint.text.dim(approvalFooter(tool, showDetails))),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
return '\n' + lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function renderApprovalDockPrompt({
|
|
99
|
+
tool, args = {}, tier, why = '', width,
|
|
100
|
+
options, selected = 0, showDetails = false,
|
|
101
|
+
} = {}) {
|
|
102
|
+
const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
|
|
103
|
+
const opts = options || defaultOptions(tier, { tool, args });
|
|
104
|
+
// Multi-line shell/python scripts auto-expand: the user cannot approve
|
|
105
|
+
// what they cannot see. "shell script · 5 lines · 309 B" as the only
|
|
106
|
+
// subject made blind approval the default. After execution the script
|
|
107
|
+
// collapses back to the one-line tool card (details stay on /last).
|
|
108
|
+
const isScriptCommand = tool === 'shell'
|
|
109
|
+
&& /\n/.test(String(args.command || args.cmd || ''));
|
|
110
|
+
const detailView = showDetails || isScriptCommand;
|
|
111
|
+
const subject = approvalDockSubject(tool, args, cols, detailView);
|
|
112
|
+
const risks = riskTerms(tool, args, tier);
|
|
113
|
+
const reason = compactReason(tool, args, why);
|
|
114
|
+
const lines = [
|
|
115
|
+
...approvalDockSubjectRows(subject),
|
|
116
|
+
...(risks.length ? [`${paint.text.dim('risk ')}${paint.state.warn(risks.join(', '))}`] : []),
|
|
117
|
+
...(reason ? [`${paint.text.dim('reason ')}${paint.text.primary(truncate(reason, 120))}`] : []),
|
|
118
|
+
paint.text.dim('Decision'),
|
|
119
|
+
...opts.map((option, index) => optionToken(option, index === selected, explicitAccent(tier))),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Show the WHOLE script when it fits — a partial script (…lines cut at
|
|
123
|
+
// the top) makes blind approval the default. Cap at half the terminal
|
|
124
|
+
// so the dock never eats the whole screen. Non-detail approvals keep
|
|
125
|
+
// the tight 8-row cap.
|
|
126
|
+
const termRows = Math.max(12, Number(process.stderr.rows) || 24);
|
|
127
|
+
const detailCap = Math.max(12, Math.floor(termRows / 2));
|
|
128
|
+
const maxRows = detailView ? Math.min(detailCap, Math.max(12, lines.length + 1)) : 8;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
prefix: '? approve › ',
|
|
132
|
+
value: truncateForDock(subject, detailView ? 1200 : 220),
|
|
133
|
+
context: `${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
|
|
134
|
+
meta: '',
|
|
135
|
+
tips: approvalFooter(tool, detailView),
|
|
136
|
+
lines,
|
|
137
|
+
maxRows,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function shellTrustHint(args = {}) {
|
|
142
|
+
const display = shellCommandDisplay(args.command || args.cmd || '');
|
|
143
|
+
const parts = String(display.command || '').trim().split(/\s+/).filter(Boolean);
|
|
144
|
+
const shape = parts.slice(0, 2).join(' ');
|
|
145
|
+
return shape ? `${shape}*` : 'similar shell commands';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function renderTrustedApproval({ tool, args = {}, scope = 'session', ruleId = '', delaySeconds = 0 } = {}) {
|
|
149
|
+
const summary = approvalSubjectSummary(tool, args);
|
|
150
|
+
const subject = `${tool || 'tool'}${summary ? ` "${truncate(summary, 80)}"` : ''}`;
|
|
151
|
+
const rule = ruleId ? ` · rule ${ruleId}` : '';
|
|
152
|
+
const delay = delaySeconds > 0 ? ` · auto-approved after ${delaySeconds}s` : '';
|
|
153
|
+
return ` ${paint.state.success('✓')} ${paint.text.primary(subject)} ${paint.text.dim(`· pre-approved (${String(scope || 'session').toLowerCase()}${rule})${delay}`)}\n`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function truncate(text, n) {
|
|
157
|
+
const s = String(text || '');
|
|
158
|
+
if (s.length <= n) return s;
|
|
159
|
+
return s.slice(0, Math.max(0, n - 1)) + '…';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Compatibility wrapper ──────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Render the unified approval prompt. Kept as a named export for older call
|
|
166
|
+
* sites/tests that imported the previous inline renderer.
|
|
167
|
+
*/
|
|
168
|
+
export function renderInlinePrompt({ tool, args = {}, tier, why = '' } = {}) {
|
|
169
|
+
return renderApprovalPrompt({ tool, args, tier, why });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { TIERS };
|
|
173
|
+
|
|
174
|
+
function tierTitle(tier) {
|
|
175
|
+
switch (tier) {
|
|
176
|
+
case TIERS.SENSITIVE_READ: return 'SENSITIVE';
|
|
177
|
+
case TIERS.PROTECTED_EDIT: return 'PROTECTED';
|
|
178
|
+
case TIERS.SHELL_DANGEROUS: return 'DANGEROUS';
|
|
179
|
+
case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
|
|
180
|
+
case TIERS.SHELL_MEDIUM: return 'MEDIUM';
|
|
181
|
+
case TIERS.NETWORK: return 'NETWORK';
|
|
182
|
+
case TIERS.LOCAL_EDIT: return 'EDIT';
|
|
183
|
+
case TIERS.SHELL_SAFE: return 'SAFE';
|
|
184
|
+
case TIERS.READ: return 'READ';
|
|
185
|
+
default: return tierLabel(tier);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function approvalTitle(tier) {
|
|
190
|
+
switch (tier) {
|
|
191
|
+
case TIERS.SENSITIVE_READ:
|
|
192
|
+
case TIERS.PROTECTED_EDIT:
|
|
193
|
+
case TIERS.SHELL_DANGEROUS:
|
|
194
|
+
case TIERS.DESTRUCTIVE:
|
|
195
|
+
return tierTitle(tier);
|
|
196
|
+
default:
|
|
197
|
+
return 'APPROVAL';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function blockHeader(title, accent) {
|
|
202
|
+
return ` ${paint.bold(accent(title))}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function blockLine(accent, text = '') {
|
|
206
|
+
return text ? ` ${text}` : ' ';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function explicitAccent(tier) {
|
|
210
|
+
return requiresExplicitApproval(tier) ? paint.brand.accent : paint.brand.data;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function subjectRows(tool, args, cols, accent) {
|
|
214
|
+
const rows = [];
|
|
215
|
+
const available = Math.max(24, cols - 5);
|
|
216
|
+
const summary = toolDisplaySummary(tool, args, {});
|
|
217
|
+
const label = subjectLabel(tool);
|
|
218
|
+
const details = subjectDetails(tool, args, summary, Math.max(24, available - visibleWidth(label) - 1));
|
|
219
|
+
|
|
220
|
+
if (details.length === 1 && visibleWidth(`${label} ${details[0]}`) <= available) {
|
|
221
|
+
rows.push(blockLine(accent, `${label} ${paint.text.primary(details[0])}`));
|
|
222
|
+
return rows;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
rows.push(blockLine(accent, label));
|
|
226
|
+
for (const line of subjectDetails(tool, args, summary, Math.max(24, available - 2))) {
|
|
227
|
+
rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.primary(line)}`));
|
|
228
|
+
}
|
|
229
|
+
return rows;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function subjectLabel(tool) {
|
|
233
|
+
const label = toolDisplayLabel(tool);
|
|
234
|
+
if (tool === 'shell') {
|
|
235
|
+
return `${paint.text.dim('• shell ·')} ${paint.text.primary(label)}`;
|
|
236
|
+
}
|
|
237
|
+
return `${icon(tool)} ${paint.text.primary(label)}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function decisionRows(opts, selected, accent) {
|
|
241
|
+
const rows = [blockLine(accent, paint.text.dim('Decision'))];
|
|
242
|
+
for (let i = 0; i < opts.length; i++) {
|
|
243
|
+
rows.push(blockLine(accent, optionToken(opts[i], i === selected, accent)));
|
|
244
|
+
}
|
|
245
|
+
return rows;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function reasonRows(tool, args, why, cols, accent) {
|
|
249
|
+
const reason = compactReason(tool, args, why);
|
|
250
|
+
if (!reason) return [];
|
|
251
|
+
const label = paint.text.dim('reason ');
|
|
252
|
+
const firstWidth = Math.max(20, cols - 5 - visibleWidth(label));
|
|
253
|
+
const restWidth = Math.max(20, cols - 5 - 7);
|
|
254
|
+
const wrapped = wrapText(reason, firstWidth);
|
|
255
|
+
const rows = [];
|
|
256
|
+
wrapped.forEach((line, index) => {
|
|
257
|
+
if (index === 0) {
|
|
258
|
+
rows.push(blockLine(accent, `${label}${paint.text.primary(line)}`));
|
|
259
|
+
} else {
|
|
260
|
+
for (const rest of wrapText(line, restWidth)) {
|
|
261
|
+
rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.primary(rest)}`));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return rows;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function compactReason(tool, args = {}, why = '') {
|
|
269
|
+
const reason = String(why || '').replace(/\s+/g, ' ').trim();
|
|
270
|
+
if (!reason || tool !== 'shell') return reason;
|
|
271
|
+
|
|
272
|
+
const redundantShellPrefix = reason.match(/^Shell command requires approval:\s*(.+)$/i);
|
|
273
|
+
if (!redundantShellPrefix) return reason;
|
|
274
|
+
|
|
275
|
+
const command = String(args.command || args.cmd || '').replace(/\s+/g, ' ').trim();
|
|
276
|
+
const repeated = redundantShellPrefix[1].replace(/\s+/g, ' ').trim();
|
|
277
|
+
if (!command || repeated === command || command.startsWith(repeated) || repeated.startsWith(command)) {
|
|
278
|
+
return 'Shell command requires approval.';
|
|
279
|
+
}
|
|
280
|
+
return reason;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function riskRows(tool, args = {}, tier, accent) {
|
|
284
|
+
const terms = riskTerms(tool, args, tier);
|
|
285
|
+
if (!terms.length) return [];
|
|
286
|
+
return [blockLine(accent, `${paint.text.dim('risk ')}${paint.state.warn(terms.join(', '))}`)];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function riskTerms(tool, args = {}, tier) {
|
|
290
|
+
const terms = [];
|
|
291
|
+
if (tool === 'shell') {
|
|
292
|
+
const command = String(args.command || args.cmd || '');
|
|
293
|
+
const patterns = [
|
|
294
|
+
[/rm\s+-[^\n]*r[^\n]*f|rm\s+-[^\n]*f[^\n]*r/i, 'rm -rf'],
|
|
295
|
+
[/\bsudo\b/i, 'sudo'],
|
|
296
|
+
[/\bgit\s+push\b[^\n]*(?:--force|-f)\b/i, 'force push'],
|
|
297
|
+
[/\bnpm\s+publish\b/i, 'publish'],
|
|
298
|
+
[/\b(?:kill|pkill)\b/i, 'process kill'],
|
|
299
|
+
[/\bchmod\s+777\b/i, 'chmod 777'],
|
|
300
|
+
[/\bcurl\b|\bwget\b/i, 'network download'],
|
|
301
|
+
];
|
|
302
|
+
for (const [re, label] of patterns) {
|
|
303
|
+
if (re.test(command)) terms.push(label);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (tier === TIERS.SENSITIVE_READ) terms.push('sensitive read');
|
|
307
|
+
if (tier === TIERS.PROTECTED_EDIT) terms.push('protected edit');
|
|
308
|
+
if (tier === TIERS.DESTRUCTIVE) terms.push('destructive');
|
|
309
|
+
return [...new Set(terms)].slice(0, 3);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function optionToken(option, selected, accent) {
|
|
313
|
+
const cursor = selected ? accent('▸ ') : paint.text.dim(' ');
|
|
314
|
+
const keyTag = paint.text.dim('[') + (selected ? accent(option.key) : paint.brand.data(option.key)) + paint.text.dim('] ');
|
|
315
|
+
const label = selected ? paint.bold(accent(option.label)) : paint.text.primary(option.label);
|
|
316
|
+
const hint = option.hint ? ` ${paint.text.muted(option.hint)}` : '';
|
|
317
|
+
return `${cursor}${keyTag}${label}${hint}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function subjectDetails(tool, args = {}, summary = '', available = 72) {
|
|
321
|
+
if (tool === 'shell') {
|
|
322
|
+
const command = args.command || args.cmd || summary || '';
|
|
323
|
+
const profile = shellCommandProfile(command);
|
|
324
|
+
if (profile.compact) {
|
|
325
|
+
const lines = [`$ ${profile.summary}`];
|
|
326
|
+
if (profile.cwdLabel) lines.push(`in ${profile.cwdLabel}`);
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
329
|
+
const display = shellCommandDisplay(command);
|
|
330
|
+
const lines = wrapText(display.command, Math.max(20, available - 2))
|
|
331
|
+
.map((line, index) => `${index === 0 ? '$' : '>'} ${line}`);
|
|
332
|
+
if (display.cwdLabel) lines.push(`in ${display.cwdLabel}`);
|
|
333
|
+
return lines.length ? lines : ['(empty command)'];
|
|
334
|
+
}
|
|
335
|
+
if (tool === 'write_file') {
|
|
336
|
+
const file = args.file_path || args.path || summary || '';
|
|
337
|
+
if (isSensitiveConfigPath(file)) return [`${file} · content redacted`];
|
|
338
|
+
const lineCount = typeof args.content === 'string' ? args.content.split('\n').length : null;
|
|
339
|
+
return [`${file}${lineCount ? ` · ${lineCount} lines` : ''}`];
|
|
340
|
+
}
|
|
341
|
+
if (tool === 'edit_file') {
|
|
342
|
+
const file = args.file_path || args.path || '';
|
|
343
|
+
if (isSensitiveConfigPath(file)) {
|
|
344
|
+
const details = [`${file || summary}`];
|
|
345
|
+
if (args.search || args.old_string) details.push('match: [redacted]');
|
|
346
|
+
if (args.replace || args.new_string) details.push('replace: [redacted]');
|
|
347
|
+
return details;
|
|
348
|
+
}
|
|
349
|
+
const search = String(args.search || args.old_string || '').trim();
|
|
350
|
+
const replacement = String(args.replace || args.new_string || '').trim();
|
|
351
|
+
const details = [`${file || summary}`];
|
|
352
|
+
if (search) details.push(`match: ${truncate(search, available - 7)}`);
|
|
353
|
+
if (replacement) details.push(`replace: ${truncate(replacement, available - 9)}`);
|
|
354
|
+
return details.slice(0, 4);
|
|
355
|
+
}
|
|
356
|
+
if (tool === 'delete_file') {
|
|
357
|
+
return [args.file_path || args.path || summary || ''];
|
|
358
|
+
}
|
|
359
|
+
if (tool === 'WebFetch' || tool === 'fetch_url') {
|
|
360
|
+
const url = args.url || summary || '';
|
|
361
|
+
return [`Target ${hostFromUrl(url) || url}`];
|
|
362
|
+
}
|
|
363
|
+
return wrapText(summary || JSON.stringify(args || {}), available).slice(0, 4);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function detailRows(tool, args = {}, cols, accent, showDetails) {
|
|
367
|
+
if (!showDetails || tool !== 'shell') return [];
|
|
368
|
+
const profile = shellCommandProfile(args.command || args.cmd || '');
|
|
369
|
+
const rows = [];
|
|
370
|
+
const labelWidth = 9;
|
|
371
|
+
const textWidth = Math.max(28, cols - 5 - labelWidth);
|
|
372
|
+
|
|
373
|
+
if (profile.cwdLabel) {
|
|
374
|
+
rows.push(blockLine(accent, `${paint.text.dim('cwd ')} ${paint.brand.data(profile.cwdLabel)}`));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
rows.push(blockLine(accent, paint.text.dim('details')));
|
|
378
|
+
if (profile.script?.body) {
|
|
379
|
+
const invocation = profile.script.invocation || profile.command.split('\n')[0] || profile.command;
|
|
380
|
+
for (const line of wrapText(invocation, textWidth)) {
|
|
381
|
+
rows.push(blockLine(accent, `${paint.text.dim('cmd ')} ${paint.text.primary(line)}`));
|
|
382
|
+
}
|
|
383
|
+
rows.push(blockLine(accent, paint.text.dim('script ')));
|
|
384
|
+
rows.push(...numberedRows(profile.script.body, cols, accent, 120));
|
|
385
|
+
} else {
|
|
386
|
+
rows.push(...wrappedLabeledRows('cmd ', profile.command, cols, accent, 120));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return rows;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function approvalFooter(tool, showDetails) {
|
|
393
|
+
const details = tool === 'shell'
|
|
394
|
+
? ` · d ${showDetails ? 'hide details' : 'details'}`
|
|
395
|
+
: '';
|
|
396
|
+
return `↑↓ move · Enter pick · letter shortcut${details} · Esc cancel`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function approvalDockDetails(tool, args = {}, cols = 96) {
|
|
400
|
+
if (tool !== 'shell') return subjectDetails(tool, args, toolDisplaySummary(tool, args, {}), cols).join(' · ');
|
|
401
|
+
const profile = shellCommandProfile(args.command || args.cmd || '');
|
|
402
|
+
if (profile.script?.body) {
|
|
403
|
+
const invocation = profile.script.invocation || profile.command.split('\n')[0] || profile.command;
|
|
404
|
+
return [
|
|
405
|
+
`$ ${invocation}`,
|
|
406
|
+
...profile.script.body.split(/\r?\n/).slice(0, 12).map((line, index) => `${index + 1} ${line}`),
|
|
407
|
+
...(profile.script.body.split(/\r?\n/).length > 12 ? ['...'] : []),
|
|
408
|
+
].join('\n');
|
|
409
|
+
}
|
|
410
|
+
return profile.command;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function approvalDockSubject(tool, args = {}, cols = 96, showDetails = false) {
|
|
414
|
+
if (showDetails && tool === 'shell') return approvalDockDetails(tool, args, cols);
|
|
415
|
+
return subjectDetails(
|
|
416
|
+
tool,
|
|
417
|
+
args,
|
|
418
|
+
toolDisplaySummary(tool, args, {}),
|
|
419
|
+
Math.max(24, cols - 20),
|
|
420
|
+
).join(' · ');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function approvalDockSubjectRows(subject) {
|
|
424
|
+
const lines = String(subject || '').split('\n');
|
|
425
|
+
const first = lines.shift() || '';
|
|
426
|
+
// 12 continuation rows matches approvalDockDetails' script cap — a
|
|
427
|
+
// 12-line script renders fully in the approval prompt.
|
|
428
|
+
return [
|
|
429
|
+
`${paint.text.dim('? approve ›')} ${paint.text.primary(truncate(first, 160))}`,
|
|
430
|
+
...lines.slice(0, 12).map(line => `${paint.text.dim(' ')}${paint.text.primary(truncate(line, 160))}`),
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function approvalSubjectSummary(tool, args = {}) {
|
|
435
|
+
const summary = toolDisplaySummary(tool, args, {});
|
|
436
|
+
if (tool !== 'shell') return summary;
|
|
437
|
+
const display = shellCommandDisplay(args.command || args.cmd || summary || '');
|
|
438
|
+
return display.cwdLabel ? `${display.command} in ${display.cwdLabel}` : display.command;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function hostFromUrl(url) {
|
|
442
|
+
try {
|
|
443
|
+
return new URL(String(url)).host;
|
|
444
|
+
} catch {
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function wrappedLabeledRows(label, text, cols, accent, maxLines = 120) {
|
|
450
|
+
const rows = [];
|
|
451
|
+
const labelText = paint.text.dim(label);
|
|
452
|
+
const firstWidth = Math.max(28, cols - 5 - visibleWidth(labelText) - 1);
|
|
453
|
+
const restWidth = Math.max(28, cols - 5 - visibleWidth(labelText) - 1);
|
|
454
|
+
const sourceLines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
|
|
455
|
+
let emitted = 0;
|
|
456
|
+
for (const source of sourceLines) {
|
|
457
|
+
const wrapped = wrapText(source || ' ', emitted === 0 ? firstWidth : restWidth);
|
|
458
|
+
for (const line of wrapped) {
|
|
459
|
+
if (emitted >= maxLines) {
|
|
460
|
+
rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.dim('... detail truncated')}`));
|
|
461
|
+
return rows;
|
|
462
|
+
}
|
|
463
|
+
rows.push(blockLine(accent, `${emitted === 0 ? labelText : paint.text.dim(' ')} ${paint.text.primary(line)}`));
|
|
464
|
+
emitted++;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return rows;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function numberedRows(text, cols, accent, maxLines = 120) {
|
|
471
|
+
const rows = [];
|
|
472
|
+
const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
|
|
473
|
+
const width = String(Math.min(lines.length, maxLines)).length;
|
|
474
|
+
const textWidth = Math.max(28, cols - 5 - width - 2);
|
|
475
|
+
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
|
476
|
+
const n = String(i + 1).padStart(width);
|
|
477
|
+
rows.push(blockLine(accent, `${paint.text.dim(`${n} `)}${paint.text.primary(truncate(lines[i], textWidth))}`));
|
|
478
|
+
}
|
|
479
|
+
if (lines.length > maxLines) {
|
|
480
|
+
rows.push(blockLine(accent, `${paint.text.dim(`... ${lines.length - maxLines} more line(s)`)}`));
|
|
481
|
+
}
|
|
482
|
+
return rows;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function truncateForDock(text, maxChars) {
|
|
486
|
+
const value = String(text || '').trim();
|
|
487
|
+
if (value.length <= maxChars) return value;
|
|
488
|
+
return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function wrapText(text, width) {
|
|
492
|
+
const words = String(text || '').split(/\s+/).filter(Boolean);
|
|
493
|
+
if (!words.length) return [''];
|
|
494
|
+
const lines = [];
|
|
495
|
+
let line = '';
|
|
496
|
+
for (const word of words) {
|
|
497
|
+
if (!line) {
|
|
498
|
+
line = word;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if ((line + ' ' + word).length <= width) {
|
|
502
|
+
line += ' ' + word;
|
|
503
|
+
} else {
|
|
504
|
+
lines.push(line);
|
|
505
|
+
line = word;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (line) lines.push(line);
|
|
509
|
+
return lines;
|
|
510
|
+
}
|