@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,180 @@
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
+ export function cleanCallbackResult(result = {}) {
16
+ // Clean result for the backend — strip internal CLI metadata so the LLM
17
+ // sees a clear, unambiguous tool result (not noisy JSON with _tool, _output_meta, etc.)
18
+ const cleanResult = {};
19
+ for (const [key, value] of Object.entries(result || {})) {
20
+ if (!key.startsWith('_')) {
21
+ cleanResult[key] = value;
22
+ }
23
+ }
24
+ return cleanResult;
25
+ }
26
+
27
+ export function backendToolResultForLlm(toolName, cleanResult = {}) {
28
+ const rawResult = cleanCallbackResult(cleanResult);
29
+ if (toolName === 'get_project_overview' && !Object.hasOwn(rawResult, 'error')) {
30
+ return {
31
+ output: rawResult.output || '',
32
+ project_resource: rawResult.project_resource,
33
+ already_registered: rawResult.already_registered || false,
34
+ };
35
+ }
36
+ if (!Object.hasOwn(rawResult, 'error')) {
37
+ const outputText = rawResult.output || '';
38
+ if (rawResult.success && outputText) {
39
+ return { output: outputText };
40
+ }
41
+ return rawResult;
42
+ }
43
+ return rawResult;
44
+ }
45
+
46
+ export function llmToolResultContent(toolName, result = {}) {
47
+ return JSON.stringify(backendToolResultForLlm(toolName, result));
48
+ }
49
+
50
+ /**
51
+ * Send a tool execution result to the backend.
52
+ * @param {string} baseUrl
53
+ * @param {string} token
54
+ * @param {string} taskId
55
+ * @param {string} callId
56
+ * @param {Object} result - { success, output, ... }
57
+ * @returns {Promise<boolean>}
58
+ */
59
+ export async function sendCallback(baseUrl, token, taskId, callId, result) {
60
+ const url = `${baseUrl}/api/callback`;
61
+ const cleanResult = cleanCallbackResult(result);
62
+
63
+ const body = JSON.stringify({
64
+ task_id: taskId,
65
+ call_id: callId,
66
+ result: cleanResult,
67
+ });
68
+
69
+ const headers = {
70
+ 'Content-Type': 'application/json',
71
+ 'Idempotency-Key': `${taskId}:${callId}`,
72
+ };
73
+ if (token) headers['Authorization'] = `Bearer ${token}`;
74
+
75
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
76
+ try {
77
+ const controller = new AbortController();
78
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
79
+
80
+ const resp = await fetch(url, {
81
+ method: 'POST',
82
+ headers,
83
+ body,
84
+ signal: controller.signal,
85
+ });
86
+
87
+ clearTimeout(timeoutId);
88
+
89
+ if (resp.ok) return true;
90
+
91
+ // 4xx: client error, don't retry
92
+ if (resp.status >= 400 && resp.status < 500) {
93
+ return false;
94
+ }
95
+
96
+ // 5xx: server error, retry
97
+ if (attempt < MAX_RETRIES) {
98
+ await sleep(RETRY_DELAY_MS * (attempt + 1));
99
+ }
100
+ } catch (err) {
101
+ // Network error or timeout, retry
102
+ if (attempt < MAX_RETRIES) {
103
+ await sleep(RETRY_DELAY_MS * (attempt + 1));
104
+ }
105
+ }
106
+ }
107
+
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Send a "skipped" callback for rejected operations.
113
+ * @param {string} baseUrl
114
+ * @param {string} token
115
+ * @param {string} taskId
116
+ * @param {string} callId
117
+ * @param {string} message
118
+ * @returns {Promise<boolean>}
119
+ */
120
+ export async function sendSkippedCallback(baseUrl, token, taskId, callId, message) {
121
+ return sendCallback(baseUrl, token, taskId, callId, {
122
+ skipped: true,
123
+ message: message || 'User rejected operation',
124
+ success: false,
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Send an approval decision to the backend's HITL handler.
130
+ * Called when the framework emits approval_required and the CLI
131
+ * has collected the user's decision via the approval menu.
132
+ *
133
+ * @param {string} baseUrl
134
+ * @param {string} token
135
+ * @param {string} taskId
136
+ * @param {string} toolId - tool_id from the approval_required event
137
+ * @param {string} decision - "grant" or "deny"
138
+ * @param {string} [scope] - "once", "type", or "all" (for grant)
139
+ * @param {string} [reason] - optional reason
140
+ * @returns {Promise<boolean>}
141
+ */
142
+ export async function sendApprovalDecision(baseUrl, token, taskId, toolId, decision, scope = 'once', reason = '') {
143
+ const url = `${baseUrl}/api/approval_callback`;
144
+
145
+ const body = JSON.stringify({
146
+ task_id: taskId,
147
+ tool_id: toolId,
148
+ decision,
149
+ scope,
150
+ reason,
151
+ });
152
+
153
+ const headers = {
154
+ 'Content-Type': 'application/json',
155
+ 'Idempotency-Key': `${taskId}:${toolId}:${decision}`,
156
+ };
157
+ if (token) headers['Authorization'] = `Bearer ${token}`;
158
+
159
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
160
+ try {
161
+ const controller = new AbortController();
162
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
163
+
164
+ const resp = await fetch(url, {
165
+ method: 'POST',
166
+ headers,
167
+ body,
168
+ signal: controller.signal,
169
+ });
170
+
171
+ clearTimeout(timeoutId);
172
+ if (resp.ok) return true;
173
+ if (resp.status >= 400 && resp.status < 500) return false;
174
+ if (attempt < MAX_RETRIES) await sleep(RETRY_DELAY_MS * (attempt + 1));
175
+ } catch {
176
+ if (attempt < MAX_RETRIES) await sleep(RETRY_DELAY_MS * (attempt + 1));
177
+ }
178
+ }
179
+ return false;
180
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * File Checkpointing — save and restore file state before edits.
3
+ *
4
+ * Before any file edit, a checkpoint is created containing the
5
+ * original file content. The /undo command restores the last checkpoint.
6
+ * Checkpoints are stored in ~/.bahulam/projects/{hash}/checkpoints/
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import crypto from 'crypto';
12
+ import { checkpointsDir } from './paths.mjs';
13
+
14
+ export class CheckpointManager {
15
+ /**
16
+ * @param {string} [baseDir] - project root directory
17
+ */
18
+ constructor(baseDir = process.cwd()) {
19
+ this.baseDir = baseDir;
20
+ this.checkpointDir = checkpointsDir(baseDir);
21
+ this.history = []; // Stack of checkpoint IDs
22
+ this.maxCheckpoints = 50;
23
+ }
24
+
25
+ /**
26
+ * Create a checkpoint for a file before editing.
27
+ * @param {string} filePath - absolute path to the file
28
+ * @returns {string|null} checkpoint ID, or null if file doesn't exist
29
+ */
30
+ save(filePath) {
31
+ const absPath = path.resolve(filePath);
32
+
33
+ try {
34
+ const content = fs.readFileSync(absPath, 'utf-8');
35
+ const id = `ckpt_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
36
+
37
+ fs.mkdirSync(this.checkpointDir, { recursive: true });
38
+
39
+ const checkpoint = {
40
+ id,
41
+ filePath: absPath,
42
+ relativePath: path.relative(this.baseDir, absPath),
43
+ content,
44
+ timestamp: new Date().toISOString(),
45
+ size: content.length,
46
+ };
47
+
48
+ const ckptFile = path.join(this.checkpointDir, `${id}.json`);
49
+ fs.writeFileSync(ckptFile, JSON.stringify(checkpoint));
50
+
51
+ this.history.push(id);
52
+
53
+ // Trim old checkpoints
54
+ while (this.history.length > this.maxCheckpoints) {
55
+ const old = this.history.shift();
56
+ try {
57
+ fs.unlinkSync(path.join(this.checkpointDir, `${old}.json`));
58
+ } catch {
59
+ // Already deleted
60
+ }
61
+ }
62
+
63
+ return id;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Restore the most recent checkpoint (undo last edit).
71
+ * @returns {{ filePath: string, restored: boolean, id: string }|null}
72
+ */
73
+ undo() {
74
+ if (this.history.length === 0) return null;
75
+
76
+ const id = this.history.pop();
77
+ const ckptFile = path.join(this.checkpointDir, `${id}.json`);
78
+
79
+ try {
80
+ const raw = fs.readFileSync(ckptFile, 'utf-8');
81
+ const checkpoint = JSON.parse(raw);
82
+
83
+ fs.writeFileSync(checkpoint.filePath, checkpoint.content);
84
+ fs.unlinkSync(ckptFile);
85
+
86
+ return {
87
+ id: checkpoint.id,
88
+ filePath: checkpoint.filePath,
89
+ restored: true,
90
+ };
91
+ } catch (err) {
92
+ return { id, filePath: null, restored: false, error: err.message };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * List recent checkpoints.
98
+ * @param {number} [limit=10]
99
+ * @returns {Array}
100
+ */
101
+ list(limit = 10) {
102
+ const result = [];
103
+ const ids = this.history.slice(-limit).reverse();
104
+
105
+ for (const id of ids) {
106
+ try {
107
+ const raw = fs.readFileSync(
108
+ path.join(this.checkpointDir, `${id}.json`),
109
+ 'utf-8'
110
+ );
111
+ const ckpt = JSON.parse(raw);
112
+ result.push({
113
+ id: ckpt.id,
114
+ file: ckpt.relativePath,
115
+ timestamp: ckpt.timestamp,
116
+ size: ckpt.size,
117
+ });
118
+ } catch {
119
+ result.push({ id, file: '?', timestamp: '?', size: 0 });
120
+ }
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ /**
127
+ * Clear all checkpoints.
128
+ */
129
+ clear() {
130
+ this.history.length = 0;
131
+ try {
132
+ const entries = fs.readdirSync(this.checkpointDir);
133
+ for (const entry of entries) {
134
+ if (entry.startsWith('ckpt_') && entry.endsWith('.json')) {
135
+ fs.unlinkSync(path.join(this.checkpointDir, entry));
136
+ }
137
+ }
138
+ } catch {
139
+ // Directory doesn't exist
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,127 @@
1
+ const SUMMARY_PREFIX = 'Session continuity summary after /compact:';
2
+
3
+ export function parseCompactTailCount(rest = '', fallback = 8) {
4
+ const text = String(rest || '').trim();
5
+ const match = text.match(/(?:^|\s)(?:--tail=|--tail\s+)?(\d+)(?:\s|$)/);
6
+ const n = match ? Number(match[1]) : Number(fallback);
7
+ if (!Number.isFinite(n)) return fallback;
8
+ return Math.max(2, Math.min(50, Math.floor(n)));
9
+ }
10
+
11
+ export function isCompactHistory(history = []) {
12
+ return typeof history?.[2]?.content === 'string'
13
+ && history[2].content.startsWith(SUMMARY_PREFIX);
14
+ }
15
+
16
+ export function extractCompactSummary(history = []) {
17
+ const content = String(history?.[2]?.content || '');
18
+ if (!content.startsWith(SUMMARY_PREFIX)) return '';
19
+ return content.slice(SUMMARY_PREFIX.length).trim();
20
+ }
21
+
22
+ export function prepareCompactHistory({
23
+ agentHistory = [],
24
+ tailCount = 8,
25
+ minSourceMessages = 2,
26
+ } = {}) {
27
+ const history = Array.isArray(agentHistory)
28
+ ? agentHistory.filter(msg => msg && typeof msg.content === 'string' && msg.content.trim())
29
+ : [];
30
+ const beforeCount = history.length;
31
+ const retainedCount = Math.min(Math.max(2, Number(tailCount) || 8), Math.max(0, beforeCount));
32
+ const prefixCount = isCompactHistory(history) ? 3 : 0;
33
+ const sourceEnd = Math.max(prefixCount, beforeCount - retainedCount);
34
+ const sourceMessages = history.slice(prefixCount, sourceEnd);
35
+ const tail = history.slice(sourceEnd);
36
+
37
+ if (sourceMessages.length < minSourceMessages) {
38
+ return {
39
+ ok: false,
40
+ reason: beforeCount <= prefixCount + retainedCount
41
+ ? 'not enough history beyond the retained tail'
42
+ : 'not enough compactable messages',
43
+ beforeCount,
44
+ prefixCount,
45
+ sourceMessages,
46
+ tail,
47
+ retainedCount,
48
+ };
49
+ }
50
+
51
+ return {
52
+ ok: true,
53
+ beforeCount,
54
+ prefixCount,
55
+ sourceMessages,
56
+ tail,
57
+ retainedCount: tail.length,
58
+ previousSummary: extractCompactSummary(history),
59
+ };
60
+ }
61
+
62
+ export function applyCompactSummary({
63
+ prepared,
64
+ summary,
65
+ sessionId = '',
66
+ cwd = '',
67
+ originalRequest = '',
68
+ previousSourceMessageCount = 0,
69
+ now = new Date(),
70
+ } = {}) {
71
+ if (!prepared?.ok) throw new Error(prepared?.reason || 'history is not compactable');
72
+ const compactSummary = String(summary || '').trim();
73
+ if (!compactSummary) throw new Error('summary is required');
74
+ const priorCount = Math.max(0, Number(previousSourceMessageCount) || 0);
75
+ const sourceMessageCount = priorCount + prepared.sourceMessages.length;
76
+ const timestamp = now instanceof Date ? now.toISOString() : String(now || new Date().toISOString());
77
+ const firstUser = originalRequest || firstUserMessage(prepared.sourceMessages) || firstUserMessage(prepared.tail) || '(unknown)';
78
+
79
+ const metadata = [
80
+ 'Compact metadata.',
81
+ sessionId ? `Session: ${sessionId}` : '',
82
+ cwd ? `Project: ${cwd}` : '',
83
+ `Compacted at: ${timestamp}`,
84
+ `Covered live messages: ${sourceMessageCount}`,
85
+ `Retained tail messages: ${prepared.tail.length}`,
86
+ ].filter(Boolean).join('\n');
87
+
88
+ const agentHistory = [
89
+ { role: 'user', content: metadata },
90
+ { role: 'user', content: `Original user request from this compacted session:\n${firstUser}` },
91
+ { role: 'user', content: `${SUMMARY_PREFIX}\n${compactSummary}` },
92
+ ...prepared.tail,
93
+ ];
94
+
95
+ return {
96
+ agentHistory,
97
+ summary: compactSummary,
98
+ sourceMessageCount,
99
+ previousSourceMessageCount: priorCount,
100
+ beforeCount: prepared.beforeCount,
101
+ afterCount: agentHistory.length,
102
+ retainedCount: prepared.tail.length,
103
+ compactedCount: prepared.sourceMessages.length,
104
+ };
105
+ }
106
+
107
+ export function localCompactSummary(messages = []) {
108
+ const list = Array.isArray(messages) ? messages : [];
109
+ const userMessages = list.filter(m => m.role === 'user').map(m => String(m.content || '').trim()).filter(Boolean);
110
+ const assistantMessages = list.filter(m => m.role === 'assistant').map(m => String(m.content || '').trim()).filter(Boolean);
111
+ return [
112
+ 'Local compact summary from live conversation context.',
113
+ userMessages.length ? `User requests (${userMessages.length}):` : '',
114
+ ...userMessages.slice(-8).map(text => `- ${truncate(text, 500)}`),
115
+ assistantMessages.length ? `Assistant progress (${assistantMessages.length}):` : '',
116
+ ...assistantMessages.slice(-8).map(text => `- ${truncate(text, 700)}`),
117
+ ].filter(Boolean).join('\n');
118
+ }
119
+
120
+ function firstUserMessage(messages = []) {
121
+ return messages.find(m => m?.role === 'user' && typeof m.content === 'string')?.content || '';
122
+ }
123
+
124
+ function truncate(text, max) {
125
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
126
+ return value.length > max ? value.slice(0, max - 3) + '...' : value;
127
+ }
@@ -0,0 +1,54 @@
1
+ import { contextToPromptBlock } from './project-context-loader.mjs';
2
+
3
+ export function buildContextEnvelope({
4
+ cwd = process.cwd(),
5
+ command = null,
6
+ args = [],
7
+ source = 'repl',
8
+ dryRun = false,
9
+ aliasesResolved = [],
10
+ effectivePolicy,
11
+ projectContext,
12
+ activeHints = [],
13
+ projectResources = [],
14
+ agentContext = {},
15
+ } = {}) {
16
+ const policy = effectivePolicy?.policy || {};
17
+ const commandTimeouts = policy.commands?.timeouts || {};
18
+ const enabled = policy.commands?.enabled || [];
19
+ const activeCommand = command || null;
20
+ const specificTimeout = activeCommand ? commandTimeouts[`${activeCommand}Seconds`] : null;
21
+
22
+ return {
23
+ cwd,
24
+ project_resources: projectResources,
25
+ agent_context: agentContext,
26
+ project_context: {
27
+ loaded_files: projectContext?.loaded || [],
28
+ changed_files: projectContext?.changed?.map(f => ({ label: f.label, path: f.path, hash: f.hash })) || [],
29
+ prompt_block: contextToPromptBlock(projectContext),
30
+ },
31
+ command_context: {
32
+ active_command: activeCommand,
33
+ source,
34
+ args,
35
+ dry_run: dryRun,
36
+ aliases_resolved: aliasesResolved,
37
+ runtime_limits: {
38
+ command_timeout_seconds: specificTimeout || commandTimeouts.defaultSeconds || 300,
39
+ tool_timeout_seconds: 120,
40
+ approval_timeout_seconds: null,
41
+ },
42
+ },
43
+ effective_options: {
44
+ commands_enabled: enabled,
45
+ plan_owner: policy.planning?.owner || 'auto',
46
+ hitl_default_scope: policy.hitl?.defaultScope || 'once',
47
+ reask_after_minutes: policy.hitl?.reaskAfterMinutes ?? 30,
48
+ hook_timeout_seconds: policy.hooks?.timeoutSeconds ?? 5,
49
+ },
50
+ context_hints: activeHints,
51
+ available_skills: projectContext?.available_skills || [],
52
+ task_state: projectContext?.task_state || [],
53
+ };
54
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Context Manager — tracks token usage and compacts conversation history.
3
+ *
4
+ * Features:
5
+ * - Proper token estimation (4 chars ~ 1 token for English)
6
+ * - Micro-compaction (remove stale tool results older than 5 turns)
7
+ * - Keep system prompt and recent 3 turns intact during compaction
8
+ * - Track pre/post compaction token counts
9
+ */
10
+
11
+ const DEFAULT_MAX_TOKENS = 180000; // ~200k model limit with buffer
12
+ const COMPACT_THRESHOLD = 0.80;
13
+ const CHARS_PER_TOKEN = 4; // rough estimate for English text
14
+ const STALE_TOOL_RESULT_TURNS = 5; // tool results older than this are micro-compacted
15
+
16
+ export class ContextManager {
17
+ /**
18
+ * @param {number} maxTokens - Maximum tokens for context window
19
+ */
20
+ constructor(maxTokens = DEFAULT_MAX_TOKENS) {
21
+ this.maxTokens = maxTokens;
22
+ this.threshold = COMPACT_THRESHOLD;
23
+ this.compactionCount = 0;
24
+ this.lastPreCompactTokens = 0;
25
+ this.lastPostCompactTokens = 0;
26
+ }
27
+
28
+ /**
29
+ * Estimate token count for a message array.
30
+ * Uses character-based heuristic (no external tokenizer dependency).
31
+ * @param {Array} messages - conversation messages
32
+ * @returns {number} estimated token count
33
+ */
34
+ getTokenCount(messages) {
35
+ let chars = 0;
36
+ for (const msg of messages) {
37
+ // Role overhead (~4 tokens)
38
+ chars += 16;
39
+
40
+ if (typeof msg.content === 'string') {
41
+ chars += msg.content.length;
42
+ } else if (Array.isArray(msg.content)) {
43
+ for (const block of msg.content) {
44
+ if (block.type === 'text') chars += (block.text || '').length;
45
+ else if (block.type === 'tool_result') chars += (block.content || '').length;
46
+ else if (block.type === 'tool_use') chars += JSON.stringify(block.input || {}).length + 20;
47
+ else if (block.type === 'thinking') chars += (block.thinking || '').length;
48
+ else chars += JSON.stringify(block).length;
49
+ }
50
+ }
51
+ }
52
+ return Math.ceil(chars / CHARS_PER_TOKEN);
53
+ }
54
+
55
+ /**
56
+ * Check if compaction is needed.
57
+ * @param {Array} messages - current conversation messages
58
+ * @returns {boolean}
59
+ */
60
+ shouldCompact(messages) {
61
+ const tokenCount = this.getTokenCount(messages);
62
+ return tokenCount >= this.maxTokens * this.threshold;
63
+ }
64
+
65
+ /**
66
+ * Micro-compact: remove verbose tool results from messages older than N turns.
67
+ * Keeps the tool call reference but truncates result content.
68
+ * @param {Array} messages
69
+ * @param {number} recentTurns - number of recent user/assistant pairs to preserve
70
+ * @returns {Array}
71
+ */
72
+ microCompact(messages, recentTurns = STALE_TOOL_RESULT_TURNS) {
73
+ // Count turns (each user message is roughly one turn)
74
+ let turnCount = 0;
75
+ for (let i = messages.length - 1; i >= 0; i--) {
76
+ if (messages[i].role === 'user') turnCount++;
77
+ }
78
+
79
+ if (turnCount <= recentTurns) return messages;
80
+
81
+ // Mark the boundary: keep last recentTurns user messages intact
82
+ let usersSeen = 0;
83
+ let boundary = messages.length;
84
+ for (let i = messages.length - 1; i >= 0; i--) {
85
+ if (messages[i].role === 'user') {
86
+ usersSeen++;
87
+ if (usersSeen >= recentTurns) {
88
+ boundary = i;
89
+ break;
90
+ }
91
+ }
92
+ }
93
+
94
+ // Truncate tool results before the boundary
95
+ const result = messages.map((msg, idx) => {
96
+ if (idx >= boundary) return msg;
97
+ if (!Array.isArray(msg.content)) return msg;
98
+
99
+ const newContent = msg.content.map(block => {
100
+ if (block.type === 'tool_result' && typeof block.content === 'string' && block.content.length > 200) {
101
+ return {
102
+ ...block,
103
+ content: block.content.slice(0, 100) + '...[truncated]',
104
+ };
105
+ }
106
+ return block;
107
+ });
108
+
109
+ return { ...msg, content: newContent };
110
+ });
111
+
112
+ return result;
113
+ }
114
+
115
+ /**
116
+ * Compact messages by summarizing older history.
117
+ * Keeps the most recent N messages intact and replaces older ones
118
+ * with a summary message.
119
+ *
120
+ * @param {Array} messages - current conversation messages
121
+ * @param {number} keepRecent - number of recent messages to preserve (default 6 = ~3 turns)
122
+ * @returns {Array} compacted message array
123
+ */
124
+ compact(messages, keepRecent = 6) {
125
+ if (messages.length <= keepRecent) return messages;
126
+
127
+ this.lastPreCompactTokens = this.getTokenCount(messages);
128
+ this.compactionCount++;
129
+
130
+ // First try micro-compaction
131
+ let working = this.microCompact(messages);
132
+ if (!this.shouldCompact(working)) {
133
+ this.lastPostCompactTokens = this.getTokenCount(working);
134
+ return working;
135
+ }
136
+
137
+ // Full compaction
138
+ const oldMessages = messages.slice(0, -keepRecent);
139
+ const recentMessages = messages.slice(-keepRecent);
140
+
141
+ // Build a summary of old messages
142
+ const summaryParts = [];
143
+ for (const msg of oldMessages) {
144
+ const role = msg.role;
145
+ let text = '';
146
+ if (typeof msg.content === 'string') {
147
+ text = msg.content.slice(0, 200);
148
+ } else if (Array.isArray(msg.content)) {
149
+ text = msg.content
150
+ .map(b => {
151
+ if (b.type === 'text') return b.text?.slice(0, 100);
152
+ if (b.type === 'tool_use') return `[tool:${b.name}]`;
153
+ if (b.type === 'tool_result') return `[result:${String(b.content).slice(0, 80)}]`;
154
+ return `[${b.type}]`;
155
+ })
156
+ .filter(Boolean)
157
+ .join(' ');
158
+ }
159
+ if (text) summaryParts.push(`${role}: ${text}`);
160
+ }
161
+
162
+ const summary = {
163
+ role: 'user',
164
+ content: `[Context compacted — summary of ${oldMessages.length} earlier messages]\n` +
165
+ summaryParts.join('\n').slice(0, 2000),
166
+ };
167
+
168
+ const compacted = [summary, ...recentMessages];
169
+ this.lastPostCompactTokens = this.getTokenCount(compacted);
170
+ return compacted;
171
+ }
172
+
173
+ /**
174
+ * Add a message and auto-compact if needed.
175
+ * @param {Array} messages - mutable message array
176
+ * @param {object} msg - new message to add
177
+ * @returns {Array} possibly compacted array with new message
178
+ */
179
+ addMessage(messages, msg) {
180
+ messages.push(msg);
181
+ if (this.shouldCompact(messages)) {
182
+ return this.compact(messages);
183
+ }
184
+ return messages;
185
+ }
186
+
187
+ /**
188
+ * Get compaction statistics.
189
+ * @returns {object}
190
+ */
191
+ getStats() {
192
+ return {
193
+ compactionCount: this.compactionCount,
194
+ lastPreCompactTokens: this.lastPreCompactTokens,
195
+ lastPostCompactTokens: this.lastPostCompactTokens,
196
+ };
197
+ }
198
+ }