@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,351 @@
1
+ /**
2
+ * JSONL Writer — writes cc-lens compatible session transcripts to ~/.kepler/.
3
+ *
4
+ * Format mirrors Claude Code's ~/.claude/ JSONL structure so that
5
+ * cc-lens (CLAUDE_CONFIG_DIR=~/.orca npx cc-lens) can read Orca sessions.
6
+ *
7
+ * Design:
8
+ * - Non-blocking: buffered writes, flushed every 500ms or on turn end
9
+ * - Accumulates content + tool_use blocks during a turn
10
+ * - Writes single assistant entry on complete event
11
+ * - Tracks UUID chain for cc-lens replay
12
+ */
13
+
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import * as os from 'node:os';
17
+ import { randomUUID } from 'node:crypto';
18
+ import * as childProcessModule from 'node:child_process';
19
+
20
+ const KEPLER_DIR = process.env.KEPLER_HOME || path.join(os.homedir(), '.kepler');
21
+ const FLUSH_INTERVAL_MS = 500;
22
+
23
+ /**
24
+ * Sanitize a cwd path into a project slug for the directory name.
25
+ * /Users/sree/Sites/myproject → -Users-sree-Sites-myproject
26
+ * Mirrors the Claude Code / kepler-cli convention.
27
+ */
28
+ function sanitizePath(p) {
29
+ return p.replace(/\//g, '-').replace(/^-/, '-');
30
+ }
31
+
32
+ function normalizeToolResultContent(output) {
33
+ if (typeof output === 'string') return output;
34
+ if (output == null) return '';
35
+ try {
36
+ const serialized = JSON.stringify(output);
37
+ return typeof serialized === 'string' ? serialized : String(output);
38
+ } catch {
39
+ return String(output);
40
+ }
41
+ }
42
+
43
+ export class JsonlWriter {
44
+ /**
45
+ * @param {string} cwd — project working directory
46
+ * @param {string} version — CLI version string
47
+ */
48
+ constructor(cwd, version) {
49
+ this.cwd = cwd;
50
+ this.version = version;
51
+ this.sessionId = null; // set by setSessionId() when backend assigns it
52
+ this.slug = sanitizePath(cwd);
53
+ this.projectDir = path.join(KEPLER_DIR, 'projects', this.slug);
54
+
55
+ // UUID chain for parent linking (cc-lens replay)
56
+ this.lastUuid = null;
57
+
58
+ // Write buffer
59
+ this._buffer = [];
60
+ this._flushTimer = null;
61
+ this._flushPromise = null;
62
+ this._transcriptPath = null; // set when sessionId is known
63
+ this._ready = false;
64
+
65
+ // Turn accumulator (reset per assistant turn)
66
+ this._turnContent = []; // [{type: 'text', text: '...'}, ...]
67
+ this._turnToolCalls = []; // [{id, name, input}, ...]
68
+ this._turnToolResults = []; // [{tool_use_id, content, is_error}, ...]
69
+ this._turnUsage = null;
70
+ this._turnModel = null;
71
+
72
+ // Git branch (captured once at construction)
73
+ this._gitBranch = this._detectGitBranch();
74
+
75
+ this._ensureDir();
76
+ }
77
+
78
+ /**
79
+ * Set the session ID (called when backend returns session_info).
80
+ * Until this is called, entries are buffered with a local fallback ID.
81
+ */
82
+ setSessionId(id) {
83
+ this.sessionId = id;
84
+ this._transcriptPath = path.join(this.projectDir, `${id}.jsonl`);
85
+ this._ready = true;
86
+ // Flush any buffered entries now that we have a path
87
+ if (this._buffer.length > 0) this._flush();
88
+ }
89
+
90
+ /**
91
+ * Generate a local session ID (for --local mode or before backend assigns one).
92
+ */
93
+ ensureSessionId() {
94
+ if (!this.sessionId) {
95
+ this.setSessionId(randomUUID());
96
+ }
97
+ }
98
+
99
+ // ── Write Methods (called from REPL) ──
100
+
101
+ /**
102
+ * Write a user turn entry.
103
+ */
104
+ writeUserTurn(content) {
105
+ this.ensureSessionId();
106
+ const uuid = randomUUID();
107
+ const entry = {
108
+ type: 'user',
109
+ uuid,
110
+ parentUuid: this.lastUuid,
111
+ timestamp: new Date().toISOString(),
112
+ cwd: this.cwd,
113
+ sessionId: this.sessionId,
114
+ version: this.version,
115
+ gitBranch: this._gitBranch,
116
+ message: { role: 'user', content },
117
+ };
118
+ this._appendEntry(entry);
119
+ this.lastUuid = uuid;
120
+ }
121
+
122
+ /**
123
+ * Accumulate content during streaming (call on content/content_partial events).
124
+ */
125
+ accumulateContent(text) {
126
+ if (!text) return;
127
+ // Merge into last text block or create new one
128
+ const last = this._turnContent[this._turnContent.length - 1];
129
+ if (last && last.type === 'text') {
130
+ last.text += text;
131
+ } else {
132
+ this._turnContent.push({ type: 'text', text });
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Accumulate a tool call (call on tool_call/tool_request events).
138
+ */
139
+ accumulateToolCall(callId, toolName, args) {
140
+ this._turnToolCalls.push({
141
+ type: 'tool_use',
142
+ id: callId,
143
+ name: toolName,
144
+ input: args || {},
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Record a tool result (call on tool_done/tool_result events).
150
+ */
151
+ recordToolResult(callId, output, isError) {
152
+ this._turnToolResults.push({
153
+ tool_use_id: callId,
154
+ content: normalizeToolResultContent(output),
155
+ is_error: !!isError,
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Set usage and model for the current turn (call on complete event).
161
+ */
162
+ setTurnUsage(usage, model) {
163
+ this._turnUsage = usage || null;
164
+ this._turnModel = model || null;
165
+ }
166
+
167
+ /**
168
+ * Finalize and write the assistant turn entry + tool result entries.
169
+ * Call this on the 'complete' event.
170
+ */
171
+ flushAssistantTurn() {
172
+ this.ensureSessionId();
173
+
174
+ // Build content array: text blocks + tool_use blocks
175
+ const contentBlocks = [...this._turnContent, ...this._turnToolCalls];
176
+ if (contentBlocks.length === 0) {
177
+ this._resetTurn();
178
+ return;
179
+ }
180
+
181
+ // Simplify: if only one text block and no tools, use string content
182
+ const content = (contentBlocks.length === 1 && contentBlocks[0].type === 'text')
183
+ ? contentBlocks[0].text
184
+ : contentBlocks;
185
+
186
+ const uuid = randomUUID();
187
+ const usage = this._turnUsage || {};
188
+ const entry = {
189
+ type: 'assistant',
190
+ uuid,
191
+ parentUuid: this.lastUuid,
192
+ timestamp: new Date().toISOString(),
193
+ cwd: this.cwd,
194
+ sessionId: this.sessionId,
195
+ version: this.version,
196
+ message: {
197
+ role: 'assistant',
198
+ model: this._turnModel || undefined,
199
+ usage: {
200
+ input_tokens: usage.total_input_tokens || usage.input_tokens || 0,
201
+ output_tokens: usage.total_output_tokens || usage.output_tokens || 0,
202
+ cache_read_input_tokens: usage.cache_read_input_tokens || 0,
203
+ cache_creation_input_tokens: usage.cache_creation_input_tokens || 0,
204
+ },
205
+ content,
206
+ },
207
+ };
208
+ this._appendEntry(entry);
209
+ this.lastUuid = uuid;
210
+
211
+ // Write tool result entries (as user messages with tool_result content)
212
+ if (this._turnToolResults.length > 0) {
213
+ const toolResultUuid = randomUUID();
214
+ const toolResultEntry = {
215
+ type: 'user',
216
+ uuid: toolResultUuid,
217
+ parentUuid: uuid,
218
+ timestamp: new Date().toISOString(),
219
+ cwd: this.cwd,
220
+ sessionId: this.sessionId,
221
+ version: this.version,
222
+ message: {
223
+ role: 'user',
224
+ content: this._turnToolResults.map(r => ({
225
+ type: 'tool_result',
226
+ tool_use_id: r.tool_use_id,
227
+ content: r.content.slice(0, 5000), // truncate large outputs
228
+ is_error: r.is_error,
229
+ })),
230
+ },
231
+ };
232
+ this._appendEntry(toolResultEntry);
233
+ this.lastUuid = toolResultUuid;
234
+ }
235
+
236
+ this._resetTurn();
237
+ this._flush(); // force flush at end of turn
238
+ }
239
+
240
+ /**
241
+ * Write a prompt entry to ~/.kepler/history.jsonl.
242
+ */
243
+ writeHistory(prompt) {
244
+ const entry = {
245
+ display: prompt,
246
+ pastedContents: {},
247
+ timestamp: Date.now(),
248
+ project: this.cwd,
249
+ sessionId: this.sessionId,
250
+ };
251
+ const historyPath = path.join(KEPLER_DIR, 'history.jsonl');
252
+ fs.promises.appendFile(historyPath, JSON.stringify(entry) + '\n', { mode: 0o600 })
253
+ .catch(() => {}); // best effort
254
+ }
255
+
256
+ /**
257
+ * Final flush on session end.
258
+ */
259
+ async close() {
260
+ if (this._flushTimer) {
261
+ clearTimeout(this._flushTimer);
262
+ this._flushTimer = null;
263
+ }
264
+ await this._flush();
265
+ if (this._flushPromise) {
266
+ await this._flushPromise;
267
+ }
268
+ }
269
+
270
+ // ── Internal ──
271
+
272
+ _resetTurn() {
273
+ this._turnContent = [];
274
+ this._turnToolCalls = [];
275
+ this._turnToolResults = [];
276
+ this._turnUsage = null;
277
+ this._turnModel = null;
278
+ }
279
+
280
+ _appendEntry(entry) {
281
+ this._buffer.push(JSON.stringify(entry));
282
+ if (!this._flushTimer && this._ready) {
283
+ this._flushTimer = setTimeout(() => this._flush(), FLUSH_INTERVAL_MS);
284
+ }
285
+ }
286
+
287
+ async _flush() {
288
+ if (this._flushTimer) {
289
+ clearTimeout(this._flushTimer);
290
+ this._flushTimer = null;
291
+ }
292
+ if (this._buffer.length === 0) {
293
+ return this._flushPromise;
294
+ }
295
+ if (!this._transcriptPath) return; // no session ID yet, keep buffered
296
+
297
+ const lines = this._buffer.join('\n') + '\n';
298
+ this._buffer = [];
299
+
300
+ const pending = this._flushPromise || Promise.resolve();
301
+ const writePromise = pending.then(async () => {
302
+ try {
303
+ await fs.promises.appendFile(this._transcriptPath, lines, { mode: 0o600 });
304
+ } catch {
305
+ // If directory was deleted, try re-creating
306
+ try {
307
+ await this._ensureDirAsync();
308
+ await fs.promises.appendFile(this._transcriptPath, lines, { mode: 0o600 });
309
+ } catch {
310
+ // silent — local logging is best-effort
311
+ }
312
+ }
313
+ });
314
+
315
+ this._flushPromise = writePromise;
316
+ try {
317
+ await writePromise;
318
+ } finally {
319
+ if (this._flushPromise === writePromise) {
320
+ this._flushPromise = null;
321
+ }
322
+ }
323
+ }
324
+
325
+ _ensureDir() {
326
+ try {
327
+ fs.mkdirSync(this.projectDir, { recursive: true, mode: 0o700 });
328
+ } catch { /* ignore */ }
329
+ try {
330
+ fs.mkdirSync(path.join(KEPLER_DIR, 'projects'), { recursive: true, mode: 0o700 });
331
+ } catch { /* ignore */ }
332
+ }
333
+
334
+ async _ensureDirAsync() {
335
+ await fs.promises.mkdir(this.projectDir, { recursive: true, mode: 0o700 });
336
+ }
337
+
338
+ _detectGitBranch() {
339
+ try {
340
+ const { execSync } = childProcessModule;
341
+ return execSync('git rev-parse --abbrev-ref HEAD', {
342
+ cwd: this.cwd,
343
+ encoding: 'utf-8',
344
+ timeout: 2000,
345
+ stdio: ['ignore', 'pipe', 'ignore'],
346
+ }).trim();
347
+ } catch {
348
+ return undefined;
349
+ }
350
+ }
351
+ }