@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.
Files changed (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +101 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. 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
+ }