@axplusb/kepler 0.0.1 → 1.0.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 +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -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 +52 -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/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 +142 -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 +159 -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 +52 -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 +182 -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/claude-reader.ts +594 -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 +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -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 +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +101 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -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 +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -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/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -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/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -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 +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -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/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -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/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -0,0 +1,273 @@
|
|
|
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 * as path from 'node:path';
|
|
14
|
+
|
|
15
|
+
// ── Tool Classification ──
|
|
16
|
+
|
|
17
|
+
const WRITE_TOOLS = new Set([
|
|
18
|
+
'shell', 'write_file', 'write_project', 'edit_file', 'delete_file',
|
|
19
|
+
'validate_build', 'lint_check',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const NEVER_AUTO_APPROVE = new Set(['delete_file']);
|
|
23
|
+
|
|
24
|
+
const FORCE_APPROVAL_SHELL = [
|
|
25
|
+
/\brm\s/,
|
|
26
|
+
/\bunlink\s/,
|
|
27
|
+
/\brmdir\s/,
|
|
28
|
+
/\bgit\s+clean/,
|
|
29
|
+
/\bgit\s+reset/,
|
|
30
|
+
/\bgit\s+push.*--force/,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const RISK_LEVELS = {
|
|
34
|
+
read_file: 'none', read_files: 'none', search_code: 'none',
|
|
35
|
+
search_files: 'none', list_files: 'none', get_file_info: 'none',
|
|
36
|
+
validate_file: 'none', validate_structure: 'none',
|
|
37
|
+
write_file: 'low', write_project: 'low', edit_file: 'low',
|
|
38
|
+
lint_check: 'low', validate_build: 'medium',
|
|
39
|
+
shell: 'medium',
|
|
40
|
+
delete_file: 'high',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function assessShellRisk(command) {
|
|
44
|
+
if (!command) return 'medium';
|
|
45
|
+
if (/rm\s+-r/i.test(command)) return 'high';
|
|
46
|
+
if (/git\s+(push|reset|clean|checkout\s+\.)/i.test(command)) return 'high';
|
|
47
|
+
if (/drop\s+(table|database)/i.test(command)) return 'high';
|
|
48
|
+
if (/sudo\s/i.test(command)) return 'high';
|
|
49
|
+
if (/^(ls|cat|head|tail|less|more|wc|file|stat|tree|find|grep|rg|ag|echo|printf|pwd|whoami|date|which|type|env|printenv|uname|hostname|id|df|du|uptime|free|top|ps|lsof)/i.test(command)) return 'low';
|
|
50
|
+
if (/^git\s+(status|log|diff|show|branch|tag|remote|stash\s+list|blame|shortlog|describe|rev-parse|ls-files|ls-tree)/i.test(command)) return 'low';
|
|
51
|
+
if (/^(npm\s+(test|run|list|ls|view|info|outdated|audit)|node\s+--check|python3?\s+-m\s+py_compile|cargo\s+(check|test|clippy))/i.test(command)) return 'low';
|
|
52
|
+
return 'medium';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── ANSI helpers ──
|
|
56
|
+
|
|
57
|
+
const RST = '\x1b[0m';
|
|
58
|
+
const DIM = '\x1b[2m';
|
|
59
|
+
const BOLD = '\x1b[1m';
|
|
60
|
+
const CYAN = '\x1b[36m';
|
|
61
|
+
const GREEN = '\x1b[32m';
|
|
62
|
+
const RED = '\x1b[31m';
|
|
63
|
+
const YELLOW = '\x1b[33m';
|
|
64
|
+
const GRAY = '\x1b[90m';
|
|
65
|
+
const WHITE = '\x1b[37m';
|
|
66
|
+
|
|
67
|
+
const write = (s) => process.stderr.write(s);
|
|
68
|
+
|
|
69
|
+
// ── Tool Description ──
|
|
70
|
+
|
|
71
|
+
function shortPath(p) {
|
|
72
|
+
if (!p) return '';
|
|
73
|
+
const cwd = process.cwd();
|
|
74
|
+
return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toolSummary(toolName, args) {
|
|
78
|
+
switch (toolName) {
|
|
79
|
+
case 'shell':
|
|
80
|
+
return args.command || '(empty)';
|
|
81
|
+
case 'write_file':
|
|
82
|
+
return shortPath(args.path || args.file_path || '');
|
|
83
|
+
case 'write_project': {
|
|
84
|
+
const files = args.files || [];
|
|
85
|
+
if (files.length === 0) return 'batch write';
|
|
86
|
+
return files.map(f => shortPath(f.path || f.file_path || '')).join(', ');
|
|
87
|
+
}
|
|
88
|
+
case 'edit_file': {
|
|
89
|
+
const fp = shortPath(args.path || args.file_path || '');
|
|
90
|
+
const search = (args.search || '').slice(0, 30);
|
|
91
|
+
return search ? `${fp} "${search}..."` : fp;
|
|
92
|
+
}
|
|
93
|
+
case 'delete_file':
|
|
94
|
+
return shortPath(args.path || args.file_path || '');
|
|
95
|
+
case 'read_file':
|
|
96
|
+
case 'read_files':
|
|
97
|
+
return shortPath(args.file_path || args.path || (args.file_paths || [])[0] || '');
|
|
98
|
+
case 'search_code':
|
|
99
|
+
return `"${args.query || args.pattern || ''}"`;
|
|
100
|
+
case 'list_files':
|
|
101
|
+
return args.pattern || args.path || '';
|
|
102
|
+
default:
|
|
103
|
+
return Object.values(args || {}).filter(v => typeof v === 'string').join(', ').slice(0, 50) || '';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Approval Manager ──
|
|
108
|
+
|
|
109
|
+
export class ApprovalManager {
|
|
110
|
+
constructor({ autoApprove = false, planMode = false } = {}) {
|
|
111
|
+
this.autoApprove = autoApprove;
|
|
112
|
+
this.planMode = planMode;
|
|
113
|
+
this.approveAll = false;
|
|
114
|
+
this.approvedToolTypes = new Set();
|
|
115
|
+
this.history = [];
|
|
116
|
+
this._rl = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setReadline(rl) { this._rl = rl; }
|
|
120
|
+
|
|
121
|
+
setExecutionHooks({ onPause, onResume } = {}) {
|
|
122
|
+
this._execPause = onPause || null;
|
|
123
|
+
this._execResume = onResume || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
revoke() {
|
|
127
|
+
const wasActive = this.approveAll || this.approvedToolTypes.size > 0;
|
|
128
|
+
this.approveAll = false;
|
|
129
|
+
this.approvedToolTypes.clear();
|
|
130
|
+
return wasActive;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getModeLabel() {
|
|
134
|
+
if (this.approveAll) return `${GREEN}allow-all${RST}`;
|
|
135
|
+
if (this.approvedToolTypes.size > 0) {
|
|
136
|
+
return `${CYAN}auto: ${[...this.approvedToolTypes].join(', ')}${RST}`;
|
|
137
|
+
}
|
|
138
|
+
return `${DIM}ask${RST}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async check(toolName, args, requireApproval = false) {
|
|
142
|
+
if (this.planMode && WRITE_TOOLS.has(toolName)) {
|
|
143
|
+
return { approved: false, reason: `Blocked by plan mode: ${toolName}` };
|
|
144
|
+
}
|
|
145
|
+
// Auto-approve everything in headless/autoApprove mode (no TTY prompts)
|
|
146
|
+
if (this.autoApprove) {
|
|
147
|
+
this.history.push({ tool: toolName, decision: 'auto', time: Date.now() });
|
|
148
|
+
return { approved: true };
|
|
149
|
+
}
|
|
150
|
+
if (!WRITE_TOOLS.has(toolName) && !requireApproval) {
|
|
151
|
+
return { approved: true };
|
|
152
|
+
}
|
|
153
|
+
if (toolName === 'shell') {
|
|
154
|
+
const risk = assessShellRisk(args.command);
|
|
155
|
+
if (risk === 'low') {
|
|
156
|
+
this.history.push({ tool: toolName, decision: 'auto-safe', time: Date.now() });
|
|
157
|
+
return { approved: true };
|
|
158
|
+
}
|
|
159
|
+
if (FORCE_APPROVAL_SHELL.some(p => p.test(args.command || ''))) {
|
|
160
|
+
return this._prompt(toolName, args);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (NEVER_AUTO_APPROVE.has(toolName)) {
|
|
164
|
+
return this._prompt(toolName, args);
|
|
165
|
+
}
|
|
166
|
+
if (this.approveAll) {
|
|
167
|
+
this.history.push({ tool: toolName, decision: 'auto', time: Date.now() });
|
|
168
|
+
return { approved: true };
|
|
169
|
+
}
|
|
170
|
+
if (this.approvedToolTypes.has(toolName)) {
|
|
171
|
+
this.history.push({ tool: toolName, decision: 'type-auto', time: Date.now() });
|
|
172
|
+
return { approved: true };
|
|
173
|
+
}
|
|
174
|
+
return this._prompt(toolName, args);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async _prompt(toolName, args) {
|
|
178
|
+
const baseRisk = RISK_LEVELS[toolName] || 'medium';
|
|
179
|
+
const risk = toolName === 'shell' ? assessShellRisk(args.command) : baseRisk;
|
|
180
|
+
const summary = toolSummary(toolName, args);
|
|
181
|
+
const isDestructive = risk === 'high';
|
|
182
|
+
|
|
183
|
+
// ── Prompt line ──
|
|
184
|
+
if (isDestructive) {
|
|
185
|
+
write(` ${YELLOW}⚠${RST} ${DIM}Allow?${RST} ${WHITE}[y]${RST} yes ${WHITE}[n]${RST} no ${WHITE}[d]${RST} details\n`);
|
|
186
|
+
} else {
|
|
187
|
+
write(` ${DIM}?${RST} ${DIM}Allow?${RST} ${WHITE}[y]${RST} yes ${WHITE}[n]${RST} no ${WHITE}[a]${RST} always ${WHITE}[t]${RST} type ${WHITE}[d]${RST} details\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const key = await this._readKey();
|
|
191
|
+
|
|
192
|
+
switch (key) {
|
|
193
|
+
case 'y':
|
|
194
|
+
case 'Y':
|
|
195
|
+
case 'return':
|
|
196
|
+
write(` ${GREEN}✓${RST} ${DIM}${toolName}${RST} ${DIM}${summary.slice(0, 60)}${RST}\n\n`);
|
|
197
|
+
this.history.push({ tool: toolName, decision: 'yes', time: Date.now() });
|
|
198
|
+
return { approved: true };
|
|
199
|
+
|
|
200
|
+
case 'n':
|
|
201
|
+
case 'N':
|
|
202
|
+
case 'escape':
|
|
203
|
+
write(` ${RED}✗${RST} ${DIM}denied${RST}\n\n`);
|
|
204
|
+
this.history.push({ tool: toolName, decision: 'no', time: Date.now() });
|
|
205
|
+
return { approved: false, reason: 'User denied' };
|
|
206
|
+
|
|
207
|
+
case 'a':
|
|
208
|
+
case 'A':
|
|
209
|
+
if (isDestructive) return this._prompt(toolName, args);
|
|
210
|
+
this.approveAll = true;
|
|
211
|
+
write(` ${GREEN}✓✓${RST} ${DIM}allow-all activated${RST}\n\n`);
|
|
212
|
+
this.history.push({ tool: toolName, decision: 'approve-all', time: Date.now() });
|
|
213
|
+
return { approved: true };
|
|
214
|
+
|
|
215
|
+
case 't':
|
|
216
|
+
case 'T':
|
|
217
|
+
if (isDestructive) return this._prompt(toolName, args);
|
|
218
|
+
this.approvedToolTypes.add(toolName);
|
|
219
|
+
write(` ${GREEN}✓${RST} ${DIM}always allow ${toolName}${RST}\n\n`);
|
|
220
|
+
this.history.push({ tool: toolName, decision: 'type-approve', time: Date.now() });
|
|
221
|
+
return { approved: true };
|
|
222
|
+
|
|
223
|
+
case 'd':
|
|
224
|
+
case 'D':
|
|
225
|
+
write(`\n${DIM}${JSON.stringify(args, null, 2)}${RST}\n\n`);
|
|
226
|
+
return this._prompt(toolName, args);
|
|
227
|
+
|
|
228
|
+
default:
|
|
229
|
+
return this._prompt(toolName, args);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_readKey() {
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
if (!process.stdin.isTTY) {
|
|
236
|
+
resolve('return');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (this._execPause) this._execPause();
|
|
241
|
+
if (this._rl) this._rl.pause();
|
|
242
|
+
|
|
243
|
+
const wasRaw = process.stdin.isRaw;
|
|
244
|
+
process.stdin.setRawMode(true);
|
|
245
|
+
process.stdin.resume();
|
|
246
|
+
process.stdin.once('data', (data) => {
|
|
247
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
248
|
+
if (this._rl) this._rl.resume();
|
|
249
|
+
if (this._execResume) this._execResume();
|
|
250
|
+
|
|
251
|
+
const bytes = [...data];
|
|
252
|
+
const str = data.toString();
|
|
253
|
+
|
|
254
|
+
if (bytes[0] === 0x03) process.exit(0);
|
|
255
|
+
if (bytes[0] === 0x1b) { resolve('escape'); return; }
|
|
256
|
+
if (str === '\r' || str === '\n') { resolve('return'); return; }
|
|
257
|
+
resolve(str);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
getSummary() {
|
|
263
|
+
const approved = this.history.filter(h => h.decision !== 'no').length;
|
|
264
|
+
const denied = this.history.filter(h => h.decision === 'no').length;
|
|
265
|
+
return {
|
|
266
|
+
total: this.history.length,
|
|
267
|
+
approved,
|
|
268
|
+
denied,
|
|
269
|
+
autoApproveAll: this.approveAll,
|
|
270
|
+
autoApprovedTypes: [...this.approvedToolTypes],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend URL resolver — auto-detects the correct backend based on environment.
|
|
3
|
+
*
|
|
4
|
+
* Priority:
|
|
5
|
+
* 1. TARANG_BACKEND_URL env var (explicit override, for dev/admin testing)
|
|
6
|
+
* 2. TARANG_ENV or NODE_ENV → mapped to known URLs
|
|
7
|
+
* 3. Default: production
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BACKEND_URLS = {
|
|
11
|
+
local: 'http://127.0.0.1:8000',
|
|
12
|
+
development: 'https://tarang-backend-development.up.railway.app',
|
|
13
|
+
production: 'https://tarang-backend-intl-web-app-production.up.railway.app',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Aliases
|
|
17
|
+
BACKEND_URLS.dev = BACKEND_URLS.development;
|
|
18
|
+
BACKEND_URLS.prod = BACKEND_URLS.production;
|
|
19
|
+
|
|
20
|
+
const WEB_URLS = {
|
|
21
|
+
local: 'http://localhost:3000',
|
|
22
|
+
development: 'https://devtarang.ai',
|
|
23
|
+
production: 'https://devtarang.ai',
|
|
24
|
+
};
|
|
25
|
+
WEB_URLS.dev = WEB_URLS.development;
|
|
26
|
+
WEB_URLS.prod = WEB_URLS.production;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the web dashboard URL from environment.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function resolveWebUrl() {
|
|
33
|
+
if (process.env.TARANG_WEB_URL) {
|
|
34
|
+
return process.env.TARANG_WEB_URL.replace(/\/$/, '');
|
|
35
|
+
}
|
|
36
|
+
const env = (process.env.TARANG_ENV || process.env.NODE_ENV || 'production').toLowerCase();
|
|
37
|
+
return WEB_URLS[env] || WEB_URLS.production;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the backend URL from environment.
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function resolveBackendUrl() {
|
|
45
|
+
// 1. Explicit env var override (for dev/admin testing)
|
|
46
|
+
if (process.env.TARANG_BACKEND_URL) {
|
|
47
|
+
return process.env.TARANG_BACKEND_URL.replace(/\/$/, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Environment-based detection
|
|
51
|
+
const env = (process.env.TARANG_ENV || process.env.NODE_ENV || 'production').toLowerCase();
|
|
52
|
+
const url = BACKEND_URLS[env];
|
|
53
|
+
if (url) return url;
|
|
54
|
+
|
|
55
|
+
// 3. Fallback to production
|
|
56
|
+
return BACKEND_URLS.production;
|
|
57
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Caching — implements cache_control for Anthropic API.
|
|
3
|
+
*
|
|
4
|
+
* Adds cache_control: { type: "ephemeral" } to system prompt blocks
|
|
5
|
+
* that are static (like CLAUDE.md content), allowing the API to
|
|
6
|
+
* cache them and reduce input token costs.
|
|
7
|
+
*
|
|
8
|
+
* Tracks cache_read_tokens and cache_creation_tokens.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class PromptCache {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.stats = {
|
|
14
|
+
cacheCreationTokens: 0,
|
|
15
|
+
cacheReadTokens: 0,
|
|
16
|
+
totalRequests: 0,
|
|
17
|
+
cacheHits: 0,
|
|
18
|
+
cacheMisses: 0,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Apply cache control to system prompt blocks.
|
|
24
|
+
* Static content (CLAUDE.md, tool definitions) gets ephemeral cache markers.
|
|
25
|
+
*
|
|
26
|
+
* @param {string|Array} systemPrompt - system prompt content
|
|
27
|
+
* @returns {Array} system prompt blocks with cache_control
|
|
28
|
+
*/
|
|
29
|
+
applyCacheControl(systemPrompt) {
|
|
30
|
+
if (typeof systemPrompt === 'string') {
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
type: 'text',
|
|
34
|
+
text: systemPrompt,
|
|
35
|
+
cache_control: { type: 'ephemeral' },
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(systemPrompt)) {
|
|
41
|
+
return systemPrompt.map((block, i) => {
|
|
42
|
+
if (typeof block === 'string') {
|
|
43
|
+
return {
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: block,
|
|
46
|
+
cache_control: { type: 'ephemeral' },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Only cache the first block (usually CLAUDE.md) and tool defs
|
|
50
|
+
if (i === 0 || block.cacheable) {
|
|
51
|
+
return { ...block, cache_control: { type: 'ephemeral' } };
|
|
52
|
+
}
|
|
53
|
+
return block;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return systemPrompt;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update cache stats from API response usage data.
|
|
62
|
+
* @param {object} usage - API response usage object
|
|
63
|
+
*/
|
|
64
|
+
updateStats(usage) {
|
|
65
|
+
this.stats.totalRequests++;
|
|
66
|
+
if (usage) {
|
|
67
|
+
if (usage.cache_creation_input_tokens) {
|
|
68
|
+
this.stats.cacheCreationTokens += usage.cache_creation_input_tokens;
|
|
69
|
+
this.stats.cacheMisses++;
|
|
70
|
+
}
|
|
71
|
+
if (usage.cache_read_input_tokens) {
|
|
72
|
+
this.stats.cacheReadTokens += usage.cache_read_input_tokens;
|
|
73
|
+
this.stats.cacheHits++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get cache efficiency stats.
|
|
80
|
+
*/
|
|
81
|
+
getStats() {
|
|
82
|
+
const hitRate = this.stats.totalRequests > 0
|
|
83
|
+
? ((this.stats.cacheHits / this.stats.totalRequests) * 100).toFixed(1)
|
|
84
|
+
: '0.0';
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
...this.stats,
|
|
88
|
+
hitRate: `${hitRate}%`,
|
|
89
|
+
tokensSaved: this.stats.cacheReadTokens,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reset stats.
|
|
95
|
+
*/
|
|
96
|
+
reset() {
|
|
97
|
+
this.stats = {
|
|
98
|
+
cacheCreationTokens: 0,
|
|
99
|
+
cacheReadTokens: 0,
|
|
100
|
+
totalRequests: 0,
|
|
101
|
+
cacheHits: 0,
|
|
102
|
+
cacheMisses: 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback Client — POST tool results back to Tarang backend.
|
|
3
|
+
* Implements retry with backoff: 2 retries, 500ms delay.
|
|
4
|
+
* 4xx: no retry. 5xx/network: retry.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MAX_RETRIES = 2;
|
|
8
|
+
const RETRY_DELAY_MS = 500;
|
|
9
|
+
const TIMEOUT_MS = 10_000;
|
|
10
|
+
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Send a tool execution result to the backend.
|
|
17
|
+
* @param {string} baseUrl
|
|
18
|
+
* @param {string} token
|
|
19
|
+
* @param {string} taskId
|
|
20
|
+
* @param {string} callId
|
|
21
|
+
* @param {Object} result - { success, output, ... }
|
|
22
|
+
* @returns {Promise<boolean>}
|
|
23
|
+
*/
|
|
24
|
+
export async function sendCallback(baseUrl, token, taskId, callId, result) {
|
|
25
|
+
const url = `${baseUrl}/api/callback`;
|
|
26
|
+
|
|
27
|
+
// Clean result for the backend — strip internal CLI metadata so the LLM
|
|
28
|
+
// sees a clear, unambiguous tool result (not noisy JSON with _tool, _output_meta, etc.)
|
|
29
|
+
const cleanResult = {};
|
|
30
|
+
for (const [key, value] of Object.entries(result)) {
|
|
31
|
+
if (!key.startsWith('_')) {
|
|
32
|
+
cleanResult[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const body = JSON.stringify({
|
|
37
|
+
task_id: taskId,
|
|
38
|
+
call_id: callId,
|
|
39
|
+
result: cleanResult,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const headers = {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
};
|
|
45
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
46
|
+
|
|
47
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
51
|
+
|
|
52
|
+
const resp = await fetch(url, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers,
|
|
55
|
+
body,
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
clearTimeout(timeoutId);
|
|
60
|
+
|
|
61
|
+
if (resp.ok) return true;
|
|
62
|
+
|
|
63
|
+
// 4xx: client error, don't retry
|
|
64
|
+
if (resp.status >= 400 && resp.status < 500) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 5xx: server error, retry
|
|
69
|
+
if (attempt < MAX_RETRIES) {
|
|
70
|
+
await sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Network error or timeout, retry
|
|
74
|
+
if (attempt < MAX_RETRIES) {
|
|
75
|
+
await sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Send a "skipped" callback for rejected operations.
|
|
85
|
+
* @param {string} baseUrl
|
|
86
|
+
* @param {string} token
|
|
87
|
+
* @param {string} taskId
|
|
88
|
+
* @param {string} callId
|
|
89
|
+
* @param {string} message
|
|
90
|
+
* @returns {Promise<boolean>}
|
|
91
|
+
*/
|
|
92
|
+
export async function sendSkippedCallback(baseUrl, token, taskId, callId, message) {
|
|
93
|
+
return sendCallback(baseUrl, token, taskId, callId, {
|
|
94
|
+
skipped: true,
|
|
95
|
+
message: message || 'User rejected operation',
|
|
96
|
+
success: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Send an approval decision to the backend's HITL handler.
|
|
102
|
+
* Called when the framework emits approval_required and the CLI
|
|
103
|
+
* has collected the user's decision via the approval menu.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} baseUrl
|
|
106
|
+
* @param {string} token
|
|
107
|
+
* @param {string} taskId
|
|
108
|
+
* @param {string} toolId - tool_id from the approval_required event
|
|
109
|
+
* @param {string} decision - "grant" or "deny"
|
|
110
|
+
* @param {string} [scope] - "once", "type", or "all" (for grant)
|
|
111
|
+
* @param {string} [reason] - optional reason
|
|
112
|
+
* @returns {Promise<boolean>}
|
|
113
|
+
*/
|
|
114
|
+
export async function sendApprovalDecision(baseUrl, token, taskId, toolId, decision, scope = 'once', reason = '') {
|
|
115
|
+
const url = `${baseUrl}/api/approval_callback`;
|
|
116
|
+
|
|
117
|
+
const body = JSON.stringify({
|
|
118
|
+
task_id: taskId,
|
|
119
|
+
tool_id: toolId,
|
|
120
|
+
decision,
|
|
121
|
+
scope,
|
|
122
|
+
reason,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
126
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
127
|
+
|
|
128
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
129
|
+
try {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
132
|
+
|
|
133
|
+
const resp = await fetch(url, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers,
|
|
136
|
+
body,
|
|
137
|
+
signal: controller.signal,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
clearTimeout(timeoutId);
|
|
141
|
+
if (resp.ok) return true;
|
|
142
|
+
if (resp.status >= 400 && resp.status < 500) return false;
|
|
143
|
+
if (attempt < MAX_RETRIES) await sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
144
|
+
} catch {
|
|
145
|
+
if (attempt < MAX_RETRIES) await sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|