@bahulam/code 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +222 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. package/src/ui/transcript-block.mjs +21 -0
@@ -0,0 +1,380 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { execFileSync } from 'node:child_process';
6
+
7
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
8
+ const DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
9
+ const DEFAULT_MAX_TURN_BYTES = 20 * 1024 * 1024;
10
+
11
+ function envInt(name, fallback) {
12
+ const raw = process.env[name];
13
+ if (!raw) return fallback;
14
+ const value = Number.parseInt(raw, 10);
15
+ return Number.isFinite(value) && value > 0 ? value : fallback;
16
+ }
17
+
18
+ function expandHome(filePath) {
19
+ if (filePath === '~') return os.homedir();
20
+ if (filePath.startsWith('~/')) return path.join(os.homedir(), filePath.slice(2));
21
+ return filePath;
22
+ }
23
+
24
+ export function resolveAttachmentPath(filePath, cwd = process.cwd()) {
25
+ const expanded = expandHome(String(filePath || '').trim());
26
+ return path.resolve(cwd, expanded);
27
+ }
28
+
29
+ function trimTrailingPunctuation(value) {
30
+ return String(value || '').replace(/[),.;:!?]+$/g, '');
31
+ }
32
+
33
+ function readQuoted(input, start) {
34
+ const quote = input[start];
35
+ let out = '';
36
+ let i = start + 1;
37
+ for (; i < input.length; i++) {
38
+ const ch = input[i];
39
+ if (ch === '\\' && i + 1 < input.length) {
40
+ out += input[i + 1];
41
+ i++;
42
+ continue;
43
+ }
44
+ if (ch === quote) return { value: out, end: i + 1 };
45
+ out += ch;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function readBare(input, start) {
51
+ let i = start;
52
+ while (i < input.length && !/\s/.test(input[i])) i++;
53
+ return { value: trimTrailingPunctuation(input.slice(start, i)), end: i };
54
+ }
55
+
56
+ function looksLikeImagePath(value) {
57
+ const ext = path.extname(String(value || '').toLowerCase());
58
+ return IMAGE_EXTENSIONS.has(ext);
59
+ }
60
+
61
+ export function parseImageReferences(input, { cwd = process.cwd() } = {}) {
62
+ const text = String(input || '');
63
+ const attachments = [];
64
+ let cleaned = '';
65
+ let i = 0;
66
+
67
+ while (i < text.length) {
68
+ if (text[i] !== '@') {
69
+ cleaned += text[i++];
70
+ continue;
71
+ }
72
+
73
+ const next = text[i + 1];
74
+ let parsed = null;
75
+ if (next === '"' || next === "'") {
76
+ parsed = readQuoted(text, i + 1);
77
+ } else if (next && !/\s/.test(next)) {
78
+ parsed = readBare(text, i + 1);
79
+ }
80
+
81
+ if (!parsed || !looksLikeImagePath(parsed.value)) {
82
+ cleaned += text[i++];
83
+ continue;
84
+ }
85
+
86
+ attachments.push({
87
+ raw: parsed.value,
88
+ path: resolveAttachmentPath(parsed.value, cwd),
89
+ });
90
+ i = parsed.end;
91
+ }
92
+
93
+ return {
94
+ instruction: cleaned.replace(/\s+/g, ' ').trim(),
95
+ references: attachments,
96
+ };
97
+ }
98
+
99
+ function sniffImage(buffer) {
100
+ if (buffer.length >= 24 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
101
+ return { mime_type: 'image/png', ext: '.png' };
102
+ }
103
+ if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
104
+ return { mime_type: 'image/jpeg', ext: '.jpg' };
105
+ }
106
+ if (buffer.length >= 10 && (buffer.subarray(0, 6).toString('ascii') === 'GIF87a' || buffer.subarray(0, 6).toString('ascii') === 'GIF89a')) {
107
+ return { mime_type: 'image/gif', ext: '.gif' };
108
+ }
109
+ if (buffer.length >= 12 && buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP') {
110
+ return { mime_type: 'image/webp', ext: '.webp' };
111
+ }
112
+ return null;
113
+ }
114
+
115
+ function pngDimensions(buffer) {
116
+ if (buffer.length < 24) return {};
117
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
118
+ }
119
+
120
+ function gifDimensions(buffer) {
121
+ if (buffer.length < 10) return {};
122
+ return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) };
123
+ }
124
+
125
+ function jpegDimensions(buffer) {
126
+ let offset = 2;
127
+ while (offset + 9 < buffer.length) {
128
+ if (buffer[offset] !== 0xff) break;
129
+ const marker = buffer[offset + 1];
130
+ const length = buffer.readUInt16BE(offset + 2);
131
+ if (length < 2) break;
132
+ if ((marker >= 0xc0 && marker <= 0xc3) || (marker >= 0xc5 && marker <= 0xc7) || (marker >= 0xc9 && marker <= 0xcb) || (marker >= 0xcd && marker <= 0xcf)) {
133
+ return {
134
+ height: buffer.readUInt16BE(offset + 5),
135
+ width: buffer.readUInt16BE(offset + 7),
136
+ };
137
+ }
138
+ offset += 2 + length;
139
+ }
140
+ return {};
141
+ }
142
+
143
+ function webpDimensions(buffer) {
144
+ if (buffer.length < 30) return {};
145
+ const chunk = buffer.subarray(12, 16).toString('ascii');
146
+ if (chunk === 'VP8X' && buffer.length >= 30) {
147
+ return {
148
+ width: 1 + buffer.readUIntLE(24, 3),
149
+ height: 1 + buffer.readUIntLE(27, 3),
150
+ };
151
+ }
152
+ return {};
153
+ }
154
+
155
+ function imageDimensions(buffer, mimeType) {
156
+ if (mimeType === 'image/png') return pngDimensions(buffer);
157
+ if (mimeType === 'image/jpeg') return jpegDimensions(buffer);
158
+ if (mimeType === 'image/gif') return gifDimensions(buffer);
159
+ if (mimeType === 'image/webp') return webpDimensions(buffer);
160
+ return {};
161
+ }
162
+
163
+ export function publicAttachmentMetadata(attachment) {
164
+ if (!attachment) return null;
165
+ return {
166
+ id: attachment.id,
167
+ kind: 'image',
168
+ source: attachment.source || 'local_file',
169
+ name: attachment.name,
170
+ path: attachment.path,
171
+ mime_type: attachment.mime_type,
172
+ bytes: attachment.bytes,
173
+ width: attachment.width || null,
174
+ height: attachment.height || null,
175
+ sha256: attachment.sha256,
176
+ optimized: Boolean(attachment.optimized),
177
+ };
178
+ }
179
+
180
+ function appleScriptString(value) {
181
+ return `"${String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
182
+ }
183
+
184
+ function powershellString(value) {
185
+ return `'${String(value || '').replace(/'/g, "''")}'`;
186
+ }
187
+
188
+ function ensureClipboardDir(baseDir = os.tmpdir()) {
189
+ const dir = path.join(baseDir, 'kepler-clipboard-images');
190
+ fs.mkdirSync(dir, { recursive: true });
191
+ return dir;
192
+ }
193
+
194
+ function hasUsableFile(filePath) {
195
+ try {
196
+ return fs.statSync(filePath).isFile() && fs.statSync(filePath).size > 0;
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ export function writeClipboardImageToTemp({
203
+ baseDir = os.tmpdir(),
204
+ runner = execFileSync,
205
+ platform = process.platform,
206
+ } = {}) {
207
+ const dir = ensureClipboardDir(baseDir);
208
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
209
+ const pngPath = path.join(dir, `clipboard-${stamp}.png`);
210
+
211
+ if (platform === 'win32') {
212
+ const script = [
213
+ 'Add-Type -AssemblyName System.Windows.Forms',
214
+ 'Add-Type -AssemblyName System.Drawing',
215
+ '$image = [System.Windows.Forms.Clipboard]::GetImage()',
216
+ "if ($null -eq $image) { throw 'Clipboard does not contain an image.' }",
217
+ `$out = ${powershellString(pngPath)}`,
218
+ '$image.Save($out, [System.Drawing.Imaging.ImageFormat]::Png)',
219
+ 'Write-Output $out',
220
+ ].join('; ');
221
+
222
+ try {
223
+ const savedPath = String(runner('powershell.exe', ['-NoProfile', '-Sta', '-Command', script], {
224
+ encoding: 'utf-8',
225
+ stdio: 'pipe',
226
+ windowsHide: true,
227
+ }) || '').trim().split(/\r?\n/).pop().trim();
228
+ if (hasUsableFile(savedPath)) return savedPath;
229
+ if (hasUsableFile(pngPath)) return pngPath;
230
+ } catch (err) {
231
+ throw new Error(`Clipboard does not contain a readable image. Copy an image, or save it and use /attach <path>. ${err.message || err}`);
232
+ }
233
+ throw new Error('Clipboard image import did not produce an image file.');
234
+ }
235
+
236
+ if (platform !== 'darwin') {
237
+ throw new Error('Clipboard image import is currently supported on macOS and Windows. Save the image to a file and use /attach <path>.');
238
+ }
239
+
240
+ const tiffPath = path.join(dir, `clipboard-${stamp}.tiff`);
241
+
242
+ try {
243
+ runner('pngpaste', [pngPath], { stdio: 'pipe' });
244
+ if (hasUsableFile(pngPath)) return pngPath;
245
+ } catch {
246
+ // pngpaste is optional. Fall through to built-in macOS tools.
247
+ }
248
+
249
+ const script = [
250
+ 'try',
251
+ ' set imageData to the clipboard as «class PNGf»',
252
+ ` set outPath to ${appleScriptString(pngPath)}`,
253
+ ' set outFile to open for access POSIX file outPath with write permission',
254
+ ' set eof outFile to 0',
255
+ ' write imageData to outFile',
256
+ ' close access outFile',
257
+ ' return outPath',
258
+ 'on error',
259
+ ' try',
260
+ ' set imageData to the clipboard as «class TIFF»',
261
+ ` set outPath to ${appleScriptString(tiffPath)}`,
262
+ ' set outFile to open for access POSIX file outPath with write permission',
263
+ ' set eof outFile to 0',
264
+ ' write imageData to outFile',
265
+ ' close access outFile',
266
+ ' return outPath',
267
+ ' on error errMsg number errNum',
268
+ ' error "Clipboard does not contain a supported image." number errNum',
269
+ ' end try',
270
+ 'end try',
271
+ ];
272
+
273
+ let savedPath = '';
274
+ try {
275
+ savedPath = String(runner('osascript', script.flatMap(line => ['-e', line]), { encoding: 'utf-8', stdio: 'pipe' }) || '').trim();
276
+ } catch (err) {
277
+ throw new Error(`Clipboard does not contain a readable image. Copy an image, or save it and use /attach <path>. ${err.message || err}`);
278
+ }
279
+
280
+ if (savedPath.endsWith('.tiff')) {
281
+ try {
282
+ runner('sips', ['-s', 'format', 'png', savedPath, '--out', pngPath], { stdio: 'pipe' });
283
+ if (hasUsableFile(pngPath)) return pngPath;
284
+ } catch (err) {
285
+ throw new Error(`Clipboard image was TIFF but could not be converted to PNG: ${err.message || err}`);
286
+ }
287
+ }
288
+
289
+ if (hasUsableFile(savedPath)) return savedPath;
290
+ if (hasUsableFile(pngPath)) return pngPath;
291
+ throw new Error('Clipboard image import did not produce an image file.');
292
+ }
293
+
294
+ export function loadClipboardImageAttachment(options = {}) {
295
+ const filePath = writeClipboardImageToTemp(options);
296
+ const attachment = loadImageAttachment(filePath, options);
297
+ return { ...attachment, source: 'clipboard' };
298
+ }
299
+
300
+ export function attachmentSummaryLine(attachment) {
301
+ const dims = attachment.width && attachment.height ? `${attachment.width}x${attachment.height}` : 'unknown size';
302
+ const kb = Math.max(1, Math.round((attachment.bytes || 0) / 1024));
303
+ return `${attachment.name} ${dims} · ${kb} KB`;
304
+ }
305
+
306
+ export function loadImageAttachment(filePath, { cwd = process.cwd(), maxBytes = DEFAULT_MAX_IMAGE_BYTES } = {}) {
307
+ const resolved = resolveAttachmentPath(filePath, cwd);
308
+ const stat = fs.statSync(resolved);
309
+ if (!stat.isFile()) throw new Error(`Not a file: ${resolved}`);
310
+ if (stat.size > maxBytes) {
311
+ throw new Error(`Image exceeds ${Math.round(maxBytes / 1024 / 1024)} MB: ${resolved}`);
312
+ }
313
+
314
+ const buffer = fs.readFileSync(resolved);
315
+ const sniffed = sniffImage(buffer);
316
+ if (!sniffed) throw new Error(`Unsupported or invalid image file: ${resolved}`);
317
+
318
+ const ext = path.extname(resolved).toLowerCase();
319
+ if (ext && IMAGE_EXTENSIONS.has(ext) && ext !== sniffed.ext && !(ext === '.jpeg' && sniffed.ext === '.jpg')) {
320
+ throw new Error(`Image extension does not match file contents: ${resolved}`);
321
+ }
322
+
323
+ const dims = imageDimensions(buffer, sniffed.mime_type);
324
+ return {
325
+ id: `att_${randomUUID().replace(/-/g, '').slice(0, 12)}`,
326
+ kind: 'image',
327
+ source: 'local_file',
328
+ name: path.basename(resolved),
329
+ path: resolved,
330
+ mime_type: sniffed.mime_type,
331
+ bytes: stat.size,
332
+ width: dims.width || null,
333
+ height: dims.height || null,
334
+ sha256: createHash('sha256').update(buffer).digest('hex'),
335
+ data_base64: buffer.toString('base64'),
336
+ optimized: false,
337
+ };
338
+ }
339
+
340
+ export function prepareImageAttachments(input, {
341
+ cwd = process.cwd(),
342
+ extraPaths = [],
343
+ maxImageBytes = envInt('KEPLER_VISION_MAX_IMAGE_BYTES', DEFAULT_MAX_IMAGE_BYTES),
344
+ maxTurnBytes = envInt('KEPLER_VISION_MAX_TURN_BYTES', DEFAULT_MAX_TURN_BYTES),
345
+ } = {}) {
346
+ const parsed = parseImageReferences(input, { cwd });
347
+ const paths = [
348
+ ...parsed.references.map(ref => ref.path),
349
+ ...extraPaths.map(p => resolveAttachmentPath(p, cwd)),
350
+ ];
351
+ const uniquePaths = [...new Set(paths)];
352
+ const attachments = uniquePaths.map(p => loadImageAttachment(p, { cwd, maxBytes: maxImageBytes }));
353
+ const totalBytes = attachments.reduce((sum, att) => sum + (att.bytes || 0), 0);
354
+ if (totalBytes > maxTurnBytes) {
355
+ throw new Error(`Attached images exceed ${Math.round(maxTurnBytes / 1024 / 1024)} MB per turn`);
356
+ }
357
+ return {
358
+ instruction: parsed.instruction || String(input || '').trim(),
359
+ attachments,
360
+ metadata: attachments.map(publicAttachmentMetadata),
361
+ };
362
+ }
363
+
364
+ export function appendVisionAnalysisToInstruction(instruction, analysis) {
365
+ const summary = String(analysis?.summary || '').trim();
366
+ if (!summary) return String(instruction || '');
367
+ const attachments = Array.isArray(analysis.attachments) ? analysis.attachments : [];
368
+ const lines = attachments.map((att, index) => {
369
+ const dims = att.width && att.height ? `${att.width}x${att.height}` : 'unknown dimensions';
370
+ return `${index + 1}. ${att.name || att.id || 'image'} (${att.mime_type || 'image'}, ${dims}, sha256=${String(att.sha256 || '').slice(0, 12)})`;
371
+ });
372
+ return [
373
+ String(instruction || '').trim(),
374
+ '',
375
+ '[Vision analysis]',
376
+ 'The primary coding agent does not receive raw image pixels. It receives this technical image analysis:',
377
+ lines.length ? `Images:\n${lines.join('\n')}` : '',
378
+ summary,
379
+ ].filter(Boolean).join('\n');
380
+ }
@@ -0,0 +1,55 @@
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:8150',
12
+ treetop: 'https://codekepler-backend-dev.kindisland-9034322d.eastus.azurecontainerapps.io',
13
+ production: 'https://codekepler-backend-prod.gentlerock-9816c6b8.centralus.azurecontainerapps.io',
14
+ };
15
+
16
+ // Aliases
17
+ BACKEND_URLS.prod = BACKEND_URLS.production;
18
+
19
+ const WEB_URLS = {
20
+ local: 'http://localhost:3100',
21
+ treetop: 'https://treetop.codekepler.ai',
22
+ production: 'https://codekepler.ai',
23
+ };
24
+ WEB_URLS.prod = WEB_URLS.production;
25
+
26
+ /**
27
+ * Resolve the web dashboard URL from environment.
28
+ * @returns {string}
29
+ */
30
+ export function resolveWebUrl() {
31
+ if (process.env.TARANG_WEB_URL) {
32
+ return process.env.TARANG_WEB_URL.replace(/\/$/, '');
33
+ }
34
+ const env = (process.env.TARANG_ENV || process.env.NODE_ENV || 'production').toLowerCase();
35
+ return WEB_URLS[env] || WEB_URLS.production;
36
+ }
37
+
38
+ /**
39
+ * Resolve the backend URL from environment.
40
+ * @returns {string}
41
+ */
42
+ export function resolveBackendUrl() {
43
+ // 1. Explicit env var override (for dev/admin testing)
44
+ if (process.env.TARANG_BACKEND_URL) {
45
+ return process.env.TARANG_BACKEND_URL.replace(/\/$/, '');
46
+ }
47
+
48
+ // 2. Environment-based detection
49
+ const env = (process.env.TARANG_ENV || process.env.NODE_ENV || 'production').toLowerCase();
50
+ const url = BACKEND_URLS[env];
51
+ if (url) return url;
52
+
53
+ // 3. Fallback to production
54
+ return BACKEND_URLS.production;
55
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * cache_control breakpoints for Anthropic prompt caching (PRD-071 Phase 2).
3
+ *
4
+ * Shared by every direct-API call site that talks to Anthropic — LocalAgent
5
+ * (Anthropic direct + OpenRouter passthrough) and agent-loop's Task sub-agents.
6
+ *
7
+ * Anthropic allows 4 breakpoints per request; we spend 3:
8
+ * 1) End of system prompt (1h TTL — persistent across long-idle sessions)
9
+ * 2) Last tool schema (1h TTL — persistent)
10
+ * 3) Second-to-last user message (5min TTL — rolls each turn, cheaper to write)
11
+ *
12
+ * The 4th slot stays reserved (attachments, future retrieval prefix).
13
+ *
14
+ * Extended 1-hour TTL is a beta — pass ANTHROPIC_BETA_HEADER on the request
15
+ * whenever any block carries ttl:'1h'.
16
+ */
17
+
18
+ export const ANTHROPIC_BETA_HEADER = 'extended-cache-ttl-2025-04-11';
19
+
20
+ const CACHE_1H = { type: 'ephemeral', ttl: '1h' };
21
+ const CACHE_5M = { type: 'ephemeral' };
22
+
23
+ /**
24
+ * Turn a `system` string into a content-block array with a cache_control
25
+ * breakpoint on the tail. If the caller already passed blocks, returns them
26
+ * unchanged. Undefined / non-string inputs pass through as-is.
27
+ */
28
+ export function cacheableSystem(systemPrompt) {
29
+ if (Array.isArray(systemPrompt)) return systemPrompt;
30
+ if (!systemPrompt || typeof systemPrompt !== 'string') return systemPrompt;
31
+ return [{ type: 'text', text: systemPrompt, cache_control: CACHE_1H }];
32
+ }
33
+
34
+ /**
35
+ * Return a copy of `tools` with cache_control on the LAST tool. Anthropic
36
+ * caches system + tools as one prefix from that breakpoint, so this single
37
+ * marker covers the whole tool schema regardless of length.
38
+ */
39
+ export function cacheableTools(tools) {
40
+ if (!Array.isArray(tools) || tools.length === 0) return tools || [];
41
+ const out = tools.slice();
42
+ const last = out[out.length - 1];
43
+ out[out.length - 1] = { ...last, cache_control: CACHE_1H };
44
+ return out;
45
+ }
46
+
47
+ /**
48
+ * Tag the SECOND-to-last user message with a 5-min cache_control breakpoint.
49
+ * Leaves the last turn write-through so the next round extends the cache
50
+ * instead of re-writing it. Returns messages unchanged when there aren't
51
+ * enough user turns yet (< 2).
52
+ *
53
+ * Handles both content shapes: string (wrapped into blocks) and block array
54
+ * (tagged on the last block).
55
+ */
56
+ export function withMessageBreakpoint(messages) {
57
+ if (!Array.isArray(messages) || messages.length < 2) return messages;
58
+ const userIdx = [];
59
+ for (let i = 0; i < messages.length; i++) {
60
+ if (messages[i].role === 'user') userIdx.push(i);
61
+ }
62
+ if (userIdx.length < 2) return messages;
63
+ const targetIdx = userIdx[userIdx.length - 2];
64
+ const msg = messages[targetIdx];
65
+
66
+ if (typeof msg.content === 'string') {
67
+ return messages.map((m, i) => i === targetIdx ? {
68
+ ...m,
69
+ content: [{ type: 'text', text: m.content, cache_control: CACHE_5M }],
70
+ } : m);
71
+ }
72
+
73
+ if (Array.isArray(msg.content) && msg.content.length > 0) {
74
+ const blocks = msg.content.slice();
75
+ const last = blocks[blocks.length - 1];
76
+ blocks[blocks.length - 1] = { ...last, cache_control: CACHE_5M };
77
+ return messages.map((m, i) => i === targetIdx ? { ...m, content: blocks } : m);
78
+ }
79
+
80
+ return messages;
81
+ }
82
+
83
+ /**
84
+ * True if the given model id needs Anthropic-style explicit cache_control
85
+ * (vs OpenAI/DeepSeek which auto-cache). Handles bare Claude ids and
86
+ * OpenRouter's `anthropic/*` prefix.
87
+ */
88
+ export function needsExplicitCacheControl(model) {
89
+ if (!model) return false;
90
+ const m = model.toLowerCase();
91
+ return m.startsWith('claude') || m.startsWith('anthropic/');
92
+ }
@@ -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
+ }