@jsayubi/ccgram 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/README.md +82 -51
- package/dist/elicitation-notify.d.ts +20 -0
- package/dist/elicitation-notify.d.ts.map +1 -0
- package/dist/elicitation-notify.js +241 -0
- package/dist/elicitation-notify.js.map +1 -0
- package/dist/enhanced-hook-notify.d.ts +8 -1
- package/dist/enhanced-hook-notify.d.ts.map +1 -1
- package/dist/enhanced-hook-notify.js +119 -5
- package/dist/enhanced-hook-notify.js.map +1 -1
- package/dist/permission-denied-notify.d.ts +11 -0
- package/dist/permission-denied-notify.d.ts.map +1 -0
- package/dist/permission-denied-notify.js +193 -0
- package/dist/permission-denied-notify.js.map +1 -0
- package/dist/permission-hook.js +43 -18
- package/dist/permission-hook.js.map +1 -1
- package/dist/pre-compact-notify.d.ts +13 -0
- package/dist/pre-compact-notify.d.ts.map +1 -0
- package/dist/pre-compact-notify.js +197 -0
- package/dist/pre-compact-notify.js.map +1 -0
- package/dist/question-notify.d.ts +6 -5
- package/dist/question-notify.d.ts.map +1 -1
- package/dist/question-notify.js +107 -23
- package/dist/question-notify.js.map +1 -1
- package/dist/setup.js +26 -10
- package/dist/setup.js.map +1 -1
- package/dist/src/types/callbacks.d.ts +11 -1
- package/dist/src/types/callbacks.d.ts.map +1 -1
- package/dist/src/types/session.d.ts +13 -1
- package/dist/src/types/session.d.ts.map +1 -1
- package/dist/src/utils/callback-parser.d.ts +7 -5
- package/dist/src/utils/callback-parser.d.ts.map +1 -1
- package/dist/src/utils/callback-parser.js +11 -5
- package/dist/src/utils/callback-parser.js.map +1 -1
- package/dist/src/utils/deep-link.d.ts +22 -0
- package/dist/src/utils/deep-link.d.ts.map +1 -0
- package/dist/src/utils/deep-link.js +43 -0
- package/dist/src/utils/deep-link.js.map +1 -0
- package/dist/src/utils/ghostty-session-manager.d.ts +81 -0
- package/dist/src/utils/ghostty-session-manager.d.ts.map +1 -0
- package/dist/src/utils/ghostty-session-manager.js +370 -0
- package/dist/src/utils/ghostty-session-manager.js.map +1 -0
- package/dist/src/utils/transcript-reader.d.ts +57 -0
- package/dist/src/utils/transcript-reader.d.ts.map +1 -0
- package/dist/src/utils/transcript-reader.js +229 -0
- package/dist/src/utils/transcript-reader.js.map +1 -0
- package/dist/workspace-router.d.ts +19 -4
- package/dist/workspace-router.d.ts.map +1 -1
- package/dist/workspace-router.js +57 -1
- package/dist/workspace-router.js.map +1 -1
- package/dist/workspace-telegram-bot.js +515 -114
- package/dist/workspace-telegram-bot.js.map +1 -1
- package/package.json +1 -1
- package/src/types/callbacks.ts +15 -1
- package/src/types/session.ts +14 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Ghostty Session Manager — controls Ghostty terminal sessions via AppleScript.
|
|
4
|
+
*
|
|
5
|
+
* Ghostty 1.3.0+ supports native AppleScript for controlling terminal windows,
|
|
6
|
+
* tabs, and splits. This backend is used when TERM_PROGRAM=ghostty and no
|
|
7
|
+
* tmux session is available.
|
|
8
|
+
*
|
|
9
|
+
* Session identity: CWD-based. Stores Map<sessionName, cwd> as handles.
|
|
10
|
+
* Text injection: input text "..." to terminal (paste-style, handles ANSI).
|
|
11
|
+
* Key sequences: arrow keys via ANSI (send key "down" is broken in 1.3.0).
|
|
12
|
+
* Modifier keys (C-c, C-u): use "send key KEY modifiers MOD to term" (no "with").
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.ghosttySessionManager = exports.GhosttySessionManager = void 0;
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const child_process_1 = require("child_process");
|
|
22
|
+
// Key name → macOS key code (sent via System Events after focus, bypasses bracketed paste)
|
|
23
|
+
// send key "down" is unsupported in Ghostty AppleScript; input text ANSI is wrapped in
|
|
24
|
+
// bracketed paste which Claude Code's TUI doesn't recognise as navigation.
|
|
25
|
+
const KEY_CODES = {
|
|
26
|
+
'Down': 125,
|
|
27
|
+
'Up': 126,
|
|
28
|
+
'Space': 49,
|
|
29
|
+
};
|
|
30
|
+
// Keys sent via "send key NAME to term" (no modifiers)
|
|
31
|
+
const SEND_KEYS = {
|
|
32
|
+
'Enter': 'enter',
|
|
33
|
+
'C-m': 'enter',
|
|
34
|
+
};
|
|
35
|
+
// Keys using "send key KEY modifiers MOD to term" (no "with" — plain param per Ghostty sdef)
|
|
36
|
+
const MODIFIER_KEYS = {
|
|
37
|
+
'C-c': { key: 'c', modifiers: 'control' },
|
|
38
|
+
'C-u': { key: 'u', modifiers: 'control' },
|
|
39
|
+
};
|
|
40
|
+
class GhosttySessionManager {
|
|
41
|
+
/** Map from session name to registered CWD. */
|
|
42
|
+
handles = new Map();
|
|
43
|
+
/**
|
|
44
|
+
* Whether Ghostty is available and running on this machine.
|
|
45
|
+
* Re-checked each call (fast) so the bot handles Ghostty launching after startup.
|
|
46
|
+
*/
|
|
47
|
+
isAvailable() {
|
|
48
|
+
if (process.platform !== 'darwin')
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
const result = (0, child_process_1.execSync)('osascript -e "application \\"Ghostty\\" is running"', {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
stdio: 'pipe',
|
|
54
|
+
timeout: 2000,
|
|
55
|
+
}).trim();
|
|
56
|
+
return result === 'true';
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Whether a handle is registered for this session name. */
|
|
63
|
+
has(name) {
|
|
64
|
+
return this.handles.has(name);
|
|
65
|
+
}
|
|
66
|
+
/** Register (or update) a session handle with its CWD. */
|
|
67
|
+
register(name, cwd) {
|
|
68
|
+
this.handles.set(name, cwd);
|
|
69
|
+
}
|
|
70
|
+
/** Remove a session handle. */
|
|
71
|
+
unregister(name) {
|
|
72
|
+
this.handles.delete(name);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Execute an AppleScript via osascript stdin pipe.
|
|
76
|
+
* Using stdin pipe avoids all shell-quoting issues and supports multi-line scripts.
|
|
77
|
+
*/
|
|
78
|
+
runScript(script) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const child = (0, child_process_1.exec)('osascript -', { timeout: 5000 }, (err, stdout) => {
|
|
81
|
+
if (err)
|
|
82
|
+
reject(err);
|
|
83
|
+
else
|
|
84
|
+
resolve(stdout.trim());
|
|
85
|
+
});
|
|
86
|
+
child.stdin.write(script);
|
|
87
|
+
child.stdin.end();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Build an AppleScript string literal expression for the given value.
|
|
92
|
+
* Handles embedded double-quotes via & quote & concatenation.
|
|
93
|
+
*/
|
|
94
|
+
buildAppleScriptString(str) {
|
|
95
|
+
const parts = str.split('"');
|
|
96
|
+
return parts.map(p => `"${p}"`).join(' & quote & ');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build an AppleScript expression that produces the given text when evaluated.
|
|
100
|
+
* Uses (character id N) for control characters/non-printable bytes,
|
|
101
|
+
* and quote for embedded double-quotes.
|
|
102
|
+
*/
|
|
103
|
+
buildAppleScriptText(text) {
|
|
104
|
+
const parts = [];
|
|
105
|
+
let current = '';
|
|
106
|
+
for (let i = 0; i < text.length; i++) {
|
|
107
|
+
const code = text.charCodeAt(i);
|
|
108
|
+
if (code >= 32 && code < 127 && code !== 34) {
|
|
109
|
+
// Printable ASCII except " — include in string literal
|
|
110
|
+
current += text[i];
|
|
111
|
+
}
|
|
112
|
+
else if (code === 34) { // "
|
|
113
|
+
if (current) {
|
|
114
|
+
parts.push(`"${current}"`);
|
|
115
|
+
current = '';
|
|
116
|
+
}
|
|
117
|
+
parts.push('quote');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Control character or non-ASCII — use character id
|
|
121
|
+
if (current) {
|
|
122
|
+
parts.push(`"${current}"`);
|
|
123
|
+
current = '';
|
|
124
|
+
}
|
|
125
|
+
parts.push(`(character id ${code})`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (current)
|
|
129
|
+
parts.push(`"${current}"`);
|
|
130
|
+
if (parts.length === 0)
|
|
131
|
+
return '""';
|
|
132
|
+
return parts.join(' & ');
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build the AppleScript fragment that finds the terminal for a registered CWD.
|
|
136
|
+
* Iterates all windows and tabs to find the terminal whose working directory matches.
|
|
137
|
+
*/
|
|
138
|
+
findTermScript(cwd) {
|
|
139
|
+
const cwdExpr = this.buildAppleScriptString(cwd);
|
|
140
|
+
return `set targetCwd to ${cwdExpr}
|
|
141
|
+
set foundTerm to missing value
|
|
142
|
+
tell application "Ghostty"
|
|
143
|
+
repeat with w in windows
|
|
144
|
+
repeat with t in tabs of w
|
|
145
|
+
repeat with term in terminals of t
|
|
146
|
+
if working directory of term is targetCwd then
|
|
147
|
+
set foundTerm to term
|
|
148
|
+
exit repeat
|
|
149
|
+
end if
|
|
150
|
+
end repeat
|
|
151
|
+
if foundTerm is not missing value then exit repeat
|
|
152
|
+
end repeat
|
|
153
|
+
if foundTerm is not missing value then exit repeat
|
|
154
|
+
end repeat
|
|
155
|
+
if foundTerm is missing value then error "No Ghostty terminal found for cwd: " & targetCwd
|
|
156
|
+
end tell`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Write raw text to a Ghostty terminal (paste-style).
|
|
160
|
+
* Handles printable ASCII, control characters (e.g. \r for Enter),
|
|
161
|
+
* and ANSI escape sequences (e.g. \x1B[B for Down arrow).
|
|
162
|
+
*/
|
|
163
|
+
async write(name, text) {
|
|
164
|
+
const cwd = this.handles.get(name);
|
|
165
|
+
if (!cwd)
|
|
166
|
+
return false;
|
|
167
|
+
try {
|
|
168
|
+
const textExpr = this.buildAppleScriptText(text);
|
|
169
|
+
const script = `${this.findTermScript(cwd)}
|
|
170
|
+
tell application "Ghostty"
|
|
171
|
+
input text ${textExpr} to foundTerm
|
|
172
|
+
end tell`;
|
|
173
|
+
await this.runScript(script);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
process.stderr.write(`[ghostty-session-manager] write failed for ${name}: ${err.message}\n`);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Write text then immediately press Return — all in one AppleScript execution.
|
|
183
|
+
* Avoids a second CWD lookup between write and Enter, which can miss the terminal.
|
|
184
|
+
*/
|
|
185
|
+
async writeLine(name, text) {
|
|
186
|
+
const cwd = this.handles.get(name);
|
|
187
|
+
if (!cwd)
|
|
188
|
+
return false;
|
|
189
|
+
try {
|
|
190
|
+
const textExpr = this.buildAppleScriptText(text);
|
|
191
|
+
const script = `${this.findTermScript(cwd)}
|
|
192
|
+
tell application "Ghostty"
|
|
193
|
+
focus foundTerm
|
|
194
|
+
input text ${textExpr} to foundTerm
|
|
195
|
+
delay 0.1
|
|
196
|
+
send key "enter" to foundTerm
|
|
197
|
+
end tell`;
|
|
198
|
+
await this.runScript(script);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
process.stderr.write(`[ghostty-session-manager] writeLine failed for ${name}: ${err.message}\n`);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Send a named key to a session.
|
|
208
|
+
* C-c and C-u use "send key ... with modifiers" (required by Ghostty AppleScript).
|
|
209
|
+
* Down, Up, Enter, Space use ANSI sequences via input text
|
|
210
|
+
* (send key "down" is broken in Ghostty 1.3.0).
|
|
211
|
+
*/
|
|
212
|
+
async sendKey(name, key) {
|
|
213
|
+
const cwd = this.handles.get(name);
|
|
214
|
+
if (!cwd)
|
|
215
|
+
return false;
|
|
216
|
+
const modKey = MODIFIER_KEYS[key];
|
|
217
|
+
if (modKey) {
|
|
218
|
+
try {
|
|
219
|
+
const script = `${this.findTermScript(cwd)}
|
|
220
|
+
tell application "Ghostty"
|
|
221
|
+
focus foundTerm
|
|
222
|
+
send key "${modKey.key}" modifiers "${modKey.modifiers}" to foundTerm
|
|
223
|
+
end tell`;
|
|
224
|
+
await this.runScript(script);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
process.stderr.write(`[ghostty-session-manager] sendKey(${key}) failed for ${name}: ${err.message}\n`);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const sendKeyName = SEND_KEYS[key];
|
|
233
|
+
if (sendKeyName) {
|
|
234
|
+
try {
|
|
235
|
+
const script = `${this.findTermScript(cwd)}
|
|
236
|
+
tell application "Ghostty"
|
|
237
|
+
focus foundTerm
|
|
238
|
+
send key "${sendKeyName}" to foundTerm
|
|
239
|
+
end tell`;
|
|
240
|
+
await this.runScript(script);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
process.stderr.write(`[ghostty-session-manager] sendKey(${key}) failed for ${name}: ${err.message}\n`);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const keyCode = KEY_CODES[key];
|
|
249
|
+
if (keyCode !== undefined) {
|
|
250
|
+
try {
|
|
251
|
+
const script = `${this.findTermScript(cwd)}
|
|
252
|
+
tell application "Ghostty"
|
|
253
|
+
focus foundTerm
|
|
254
|
+
end tell
|
|
255
|
+
tell application "System Events"
|
|
256
|
+
tell process "Ghostty"
|
|
257
|
+
key code ${keyCode}
|
|
258
|
+
end tell
|
|
259
|
+
end tell`;
|
|
260
|
+
await this.runScript(script);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
process.stderr.write(`[ghostty-session-manager] sendKey(${key}) failed for ${name}: ${err.message}\n`);
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Unknown key — pass through as text
|
|
269
|
+
return this.write(name, key);
|
|
270
|
+
}
|
|
271
|
+
/** Send Ctrl+C interrupt to the terminal. */
|
|
272
|
+
async interrupt(name) {
|
|
273
|
+
return this.sendKey(name, 'C-c');
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Capture terminal scrollback by triggering write_scrollback_file.
|
|
277
|
+
* Finds the new file in the OS temp directory and reads its content.
|
|
278
|
+
* Returns null if unavailable or no file produced within 1s.
|
|
279
|
+
*/
|
|
280
|
+
async capture(name) {
|
|
281
|
+
const cwd = this.handles.get(name);
|
|
282
|
+
if (!cwd)
|
|
283
|
+
return null;
|
|
284
|
+
const tmpDir = os_1.default.tmpdir();
|
|
285
|
+
// 1. Note existing Ghostty temp files and their mtimes
|
|
286
|
+
const existingFiles = new Map();
|
|
287
|
+
try {
|
|
288
|
+
const files = fs_1.default.readdirSync(tmpDir);
|
|
289
|
+
for (const f of files) {
|
|
290
|
+
const fl = f.toLowerCase();
|
|
291
|
+
if (fl.startsWith('ghostty') || fl.startsWith('com.mitchellh.ghostty')) {
|
|
292
|
+
try {
|
|
293
|
+
const mtime = fs_1.default.statSync(`${tmpDir}/${f}`).mtimeMs;
|
|
294
|
+
existingFiles.set(f, mtime);
|
|
295
|
+
}
|
|
296
|
+
catch { }
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch { }
|
|
301
|
+
// 2. Trigger write_scrollback_file
|
|
302
|
+
try {
|
|
303
|
+
const script = `${this.findTermScript(cwd)}
|
|
304
|
+
tell application "Ghostty"
|
|
305
|
+
perform action "write_scrollback_file" on foundTerm
|
|
306
|
+
end tell`;
|
|
307
|
+
await this.runScript(script);
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
process.stderr.write(`[ghostty-session-manager] capture trigger failed for ${name}: ${err.message}\n`);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
// 3. Wait up to 1s for a new (or updated) file to appear
|
|
314
|
+
const deadline = Date.now() + 1000;
|
|
315
|
+
while (Date.now() < deadline) {
|
|
316
|
+
try {
|
|
317
|
+
const files = fs_1.default.readdirSync(tmpDir);
|
|
318
|
+
for (const f of files) {
|
|
319
|
+
const fl = f.toLowerCase();
|
|
320
|
+
if (fl.startsWith('ghostty') || fl.startsWith('com.mitchellh.ghostty')) {
|
|
321
|
+
const filePath = `${tmpDir}/${f}`;
|
|
322
|
+
try {
|
|
323
|
+
const mtime = fs_1.default.statSync(filePath).mtimeMs;
|
|
324
|
+
const prevMtime = existingFiles.get(f);
|
|
325
|
+
if (prevMtime === undefined || mtime > prevMtime) {
|
|
326
|
+
return fs_1.default.readFileSync(filePath, 'utf8');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch { }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch { }
|
|
334
|
+
await new Promise(r => setTimeout(r, 100));
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Open a new tab in the front Ghostty window and run a command.
|
|
340
|
+
* Uses AppleScript's quoted form of for safe shell quoting of the cwd.
|
|
341
|
+
*/
|
|
342
|
+
async openNewTab(cwd, command) {
|
|
343
|
+
try {
|
|
344
|
+
// Build AppleScript expression for cwd (used with quoted form of)
|
|
345
|
+
const cwdExpr = this.buildAppleScriptString(cwd);
|
|
346
|
+
// Build AppleScript expression for the command (plain text)
|
|
347
|
+
const cmdExpr = this.buildAppleScriptText(command);
|
|
348
|
+
const script = `tell application "Ghostty"
|
|
349
|
+
if (count of windows) is 0 then error "No Ghostty windows open"
|
|
350
|
+
set w to front window
|
|
351
|
+
perform action "new_tab" on w
|
|
352
|
+
delay 0.5
|
|
353
|
+
set term to focused terminal of selected tab of w
|
|
354
|
+
focus term
|
|
355
|
+
input text "cd " & quoted form of ${cwdExpr} & " && " & ${cmdExpr} to term
|
|
356
|
+
delay 0.1
|
|
357
|
+
send key "enter" to term
|
|
358
|
+
end tell`;
|
|
359
|
+
await this.runScript(script);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
process.stderr.write(`[ghostty-session-manager] openNewTab failed: ${err.message}\n`);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
exports.GhosttySessionManager = GhosttySessionManager;
|
|
369
|
+
exports.ghosttySessionManager = new GhosttySessionManager();
|
|
370
|
+
//# sourceMappingURL=ghostty-session-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghostty-session-manager.js","sourceRoot":"","sources":["../../../src/utils/ghostty-session-manager.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;;;;AAEH,4CAAoB;AACpB,4CAAoB;AACpB,iDAA+C;AAE/C,2FAA2F;AAC3F,uFAAuF;AACvF,2EAA2E;AAC3E,MAAM,SAAS,GAA2B;IACxC,MAAM,EAAG,GAAG;IACZ,IAAI,EAAK,GAAG;IACZ,OAAO,EAAE,EAAE;CACZ,CAAC;AAEF,uDAAuD;AACvD,MAAM,SAAS,GAA2B;IACxC,OAAO,EAAE,OAAO;IAChB,KAAK,EAAI,OAAO;CACjB,CAAC;AAEF,6FAA6F;AAC7F,MAAM,aAAa,GAAuD;IACxE,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE;IACzC,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE;CAC1C,CAAC;AAEF,MAAa,qBAAqB;IAChC,+CAA+C;IACvC,OAAO,GAAwB,IAAI,GAAG,EAAE,CAAC;IAEjD;;;OAGG;IACH,WAAW;QACT,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,qDAAqD,EAAE;gBAC7E,QAAQ,EAAE,MAAM;gBAChB,KAAK,EAAE,MAAM;gBACb,OAAO,EAAE,IAAI;aACd,CAAC,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,MAAM,KAAK,MAAM,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,4DAA4D;IAC5D,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IAED,0DAA0D;IAC1D,QAAQ,CAAC,IAAY,EAAE,GAAW;QAChC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9B,CAAC;IAED,+BAA+B;IAC/B,UAAU,CAAC,IAAY;QACrB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED;;;OAGG;IACK,SAAS,CAAC,MAAc;QAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,KAAK,GAAG,IAAA,oBAAI,EAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;gBACnE,IAAI,GAAG;oBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;oBAChB,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;YAC9B,CAAC,CAAC,CAAC;YACH,KAAK,CAAC,KAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC3B,KAAK,CAAC,KAAM,CAAC,GAAG,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACK,sBAAsB,CAAC,GAAW;QACxC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC7B,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACtD,CAAC;IAED;;;;OAIG;IACK,oBAAoB,CAAC,IAAY;QACvC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,OAAO,GAAG,EAAE,CAAC;QAEjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YAChC,IAAI,IAAI,IAAI,EAAE,IAAI,IAAI,GAAG,GAAG,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;gBAC5C,uDAAuD;gBACvD,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;YACrB,CAAC;iBAAM,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI;gBAC5B,IAAI,OAAO,EAAE,CAAC;oBAAC,KAAK,CAAC,IAAI,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;gBAAC,CAAC;gBAC1D,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtB,CAAC;iBAAM,CAAC;gBACN,oDAAoD;gBACpD,IAAI,OAAO,EAAE,CAAC;oBAAC,KAAK,CAAC,IAAI,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;gBAAC,CAAC;gBAC1D,KAAK,CAAC,IAAI,CAAC,iBAAiB,IAAI,GAAG,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;QAED,IAAI,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC;QACxC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACpC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACK,cAAc,CAAC,GAAW;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC;QACjD,OAAO,oBAAoB,OAAO;;;;;;;;;;;;;;;;SAgB7B,CAAC;IACR,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,IAAY;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO,KAAK,CAAC;QAEvB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;;eAEjC,QAAQ;SACd,CAAC;YACJ,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,IAAI,KAAM,GAAa,CAAC,OAAO,IAAI,CAAC,CAAC;YACxG,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,IAAY;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO,KAAK,CAAC;QAEvB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;;;eAGjC,QAAQ;;;SAGd,CAAC;YACJ,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kDAAkD,IAAI,KAAM,GAAa,CAAC,OAAO,IAAI,CAAC,CAAC;YAC5G,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,GAAW;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO,KAAK,CAAC;QAEvB,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;;;cAGpC,MAAM,CAAC,GAAG,gBAAgB,MAAM,CAAC,SAAS;SAC/C,CAAC;gBACF,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBAC7B,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,GAAG,gBAAgB,IAAI,KAAM,GAAa,CAAC,OAAO,IAAI,CAAC,CAAC;gBAClH,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,MAAM,WAAW,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,WAAW,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;;;cAGpC,WAAW;SAChB,CAAC;gBACF,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBAC7B,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,GAAG,gBAAgB,IAAI,KAAM,GAAa,CAAC,OAAO,IAAI,CAAC,CAAC;gBAClH,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;;;;;;eAMnC,OAAO;;SAEb,CAAC;gBACF,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBAC7B,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,GAAG,gBAAgB,IAAI,KAAM,GAAa,CAAC,OAAO,IAAI,CAAC,CAAC;gBAClH,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,qCAAqC;QACrC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,6CAA6C;IAC7C,KAAK,CAAC,SAAS,CAAC,IAAY;QAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAC,IAAY;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QAEtB,MAAM,MAAM,GAAG,YAAE,CAAC,MAAM,EAAE,CAAC;QAE3B,uDAAuD;QACvD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;QAChD,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,YAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YACrC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtB,MAAM,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;gBAC3B,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,uBAAuB,CAAC,EAAE,CAAC;oBACvE,IAAI,CAAC;wBACH,MAAM,KAAK,GAAG,YAAE,CAAC,QAAQ,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;wBACpD,aAAa,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;oBAC9B,CAAC;oBAAC,MAAM,CAAC,CAAA,CAAC;gBACZ,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QAEV,mCAAmC;QACnC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;;;SAGvC,CAAC;YACJ,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,wDAAwD,IAAI,KAAM,GAAa,CAAC,OAAO,IAAI,CAAC,CAAC;YAClH,OAAO,IAAI,CAAC;QACd,CAAC;QAED,yDAAyD;QACzD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QACnC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,YAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;gBACrC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;oBACtB,MAAM,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;oBAC3B,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,uBAAuB,CAAC,EAAE,CAAC;wBACvE,MAAM,QAAQ,GAAG,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC;wBAClC,IAAI,CAAC;4BACH,MAAM,KAAK,GAAG,YAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC;4BAC5C,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;4BACvC,IAAI,SAAS,KAAK,SAAS,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;gCACjD,OAAO,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;4BAC3C,CAAC;wBACH,CAAC;wBAAC,MAAM,CAAC,CAAA,CAAC;oBACZ,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACV,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QACnD,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,GAAW,EAAE,OAAe;QAC3C,IAAI,CAAC;YACH,kEAAkE;YAClE,MAAM,OAAO,GAAG,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC;YACjD,4DAA4D;YAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;YACnD,MAAM,MAAM,GAAG;;;;;;;sCAOiB,OAAO,eAAe,OAAO;;;SAG1D,CAAC;YACJ,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gDAAiD,GAAa,CAAC,OAAO,IAAI,CAAC,CAAC;YACjG,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF;AArUD,sDAqUC;AAEY,QAAA,qBAAqB,GAAG,IAAI,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript Reader — reads Claude Code session JSONL transcripts to surface
|
|
3
|
+
* developer-useful status info (model, context %, last assistant message, etc.)
|
|
4
|
+
*
|
|
5
|
+
* Path: ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl
|
|
6
|
+
* Encoding: cwd with `/` replaced by `-` (so `/Users/foo/bar` → `-Users-foo-bar`)
|
|
7
|
+
*/
|
|
8
|
+
/** Status fields parsed from a session transcript. */
|
|
9
|
+
export interface TranscriptStatus {
|
|
10
|
+
model?: string;
|
|
11
|
+
version?: string;
|
|
12
|
+
gitBranch?: string;
|
|
13
|
+
slug?: string;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
/** Total prompt+output tokens from the most recent assistant turn. */
|
|
17
|
+
contextTokens?: number;
|
|
18
|
+
/** Inferred from model name; null if unknown. */
|
|
19
|
+
contextLimit?: number | null;
|
|
20
|
+
/** 0-100, omitted when contextLimit is null. */
|
|
21
|
+
contextPct?: number;
|
|
22
|
+
/** Plain-text snippet of the last assistant message, truncated. */
|
|
23
|
+
lastAssistantMessage?: string;
|
|
24
|
+
/** ISO timestamp of the last assistant message. */
|
|
25
|
+
lastAssistantTimestamp?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Encode a cwd into Claude Code's project directory name. */
|
|
28
|
+
export declare function encodeCwd(cwd: string): string;
|
|
29
|
+
/** Build the full path to a session's JSONL transcript. */
|
|
30
|
+
export declare function getTranscriptPath(cwd: string, sessionId: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Find the most recently modified transcript for a cwd when sessionId is unknown.
|
|
33
|
+
* Returns null if no transcripts exist for that cwd.
|
|
34
|
+
*/
|
|
35
|
+
export declare function findLatestTranscript(cwd: string): {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
path: string;
|
|
38
|
+
mtimeMs: number;
|
|
39
|
+
} | null;
|
|
40
|
+
/**
|
|
41
|
+
* Inferred context window size for known model name patterns.
|
|
42
|
+
* Returns null when we don't know the limit (caller should omit % display).
|
|
43
|
+
*
|
|
44
|
+
* Note: the [1m] suffix that enables 1M context is an API parameter — the model
|
|
45
|
+
* field in transcripts does NOT include it. So we conservatively assume 200K
|
|
46
|
+
* for Opus 4.6 unless we find a way to detect 1m mode.
|
|
47
|
+
*/
|
|
48
|
+
export declare function inferContextLimit(model: string | undefined): number | null;
|
|
49
|
+
/**
|
|
50
|
+
* Read the latest status info from a session's transcript.
|
|
51
|
+
* Returns null when the transcript file doesn't exist or is unreadable.
|
|
52
|
+
*
|
|
53
|
+
* `maxAssistantChars` truncates the last-message snippet to keep Telegram
|
|
54
|
+
* messages under their 4096-char limit.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readTranscriptStatus(cwd: string, sessionId: string | undefined, maxAssistantChars?: number): TranscriptStatus | null;
|
|
57
|
+
//# sourceMappingURL=transcript-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript-reader.d.ts","sourceRoot":"","sources":["../../../src/utils/transcript-reader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mDAAmD;IACnD,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGxE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAgB7G;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAO1E;AA6DD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,iBAAiB,GAAE,MAAY,GAC9B,gBAAgB,GAAG,IAAI,CAsFzB"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transcript Reader — reads Claude Code session JSONL transcripts to surface
|
|
4
|
+
* developer-useful status info (model, context %, last assistant message, etc.)
|
|
5
|
+
*
|
|
6
|
+
* Path: ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl
|
|
7
|
+
* Encoding: cwd with `/` replaced by `-` (so `/Users/foo/bar` → `-Users-foo-bar`)
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.encodeCwd = encodeCwd;
|
|
14
|
+
exports.getTranscriptPath = getTranscriptPath;
|
|
15
|
+
exports.findLatestTranscript = findLatestTranscript;
|
|
16
|
+
exports.inferContextLimit = inferContextLimit;
|
|
17
|
+
exports.readTranscriptStatus = readTranscriptStatus;
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
/** Encode a cwd into Claude Code's project directory name. */
|
|
22
|
+
function encodeCwd(cwd) {
|
|
23
|
+
return cwd.replace(/\//g, '-');
|
|
24
|
+
}
|
|
25
|
+
/** Build the full path to a session's JSONL transcript. */
|
|
26
|
+
function getTranscriptPath(cwd, sessionId) {
|
|
27
|
+
const encoded = encodeCwd(cwd);
|
|
28
|
+
return path_1.default.join(os_1.default.homedir(), '.claude', 'projects', encoded, `${sessionId}.jsonl`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Find the most recently modified transcript for a cwd when sessionId is unknown.
|
|
32
|
+
* Returns null if no transcripts exist for that cwd.
|
|
33
|
+
*/
|
|
34
|
+
function findLatestTranscript(cwd) {
|
|
35
|
+
const dir = path_1.default.join(os_1.default.homedir(), '.claude', 'projects', encodeCwd(cwd));
|
|
36
|
+
if (!fs_1.default.existsSync(dir))
|
|
37
|
+
return null;
|
|
38
|
+
let latest = null;
|
|
39
|
+
for (const entry of fs_1.default.readdirSync(dir)) {
|
|
40
|
+
if (!entry.endsWith('.jsonl'))
|
|
41
|
+
continue;
|
|
42
|
+
const full = path_1.default.join(dir, entry);
|
|
43
|
+
try {
|
|
44
|
+
const mtimeMs = fs_1.default.statSync(full).mtimeMs;
|
|
45
|
+
if (!latest || mtimeMs > latest.mtimeMs) {
|
|
46
|
+
latest = { sessionId: entry.replace(/\.jsonl$/, ''), path: full, mtimeMs };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
}
|
|
51
|
+
return latest;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Inferred context window size for known model name patterns.
|
|
55
|
+
* Returns null when we don't know the limit (caller should omit % display).
|
|
56
|
+
*
|
|
57
|
+
* Note: the [1m] suffix that enables 1M context is an API parameter — the model
|
|
58
|
+
* field in transcripts does NOT include it. So we conservatively assume 200K
|
|
59
|
+
* for Opus 4.6 unless we find a way to detect 1m mode.
|
|
60
|
+
*/
|
|
61
|
+
function inferContextLimit(model) {
|
|
62
|
+
if (!model)
|
|
63
|
+
return null;
|
|
64
|
+
const m = model.toLowerCase();
|
|
65
|
+
if (m.includes('opus-4-6') || m.includes('sonnet-4-6') || m.includes('haiku-4-5'))
|
|
66
|
+
return 200000;
|
|
67
|
+
if (m.includes('opus-4-5') || m.includes('sonnet-4-5') || m.includes('haiku-4-1'))
|
|
68
|
+
return 200000;
|
|
69
|
+
if (m.includes('opus') || m.includes('sonnet') || m.includes('haiku'))
|
|
70
|
+
return 200000;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Sum the tokens that count toward the context window from a `usage` object.
|
|
75
|
+
* Cache reads count — they are still in-context. Output tokens count too,
|
|
76
|
+
* because they become part of the next turn's prompt.
|
|
77
|
+
*/
|
|
78
|
+
function sumContextTokens(usage) {
|
|
79
|
+
const input = usage.input_tokens || 0;
|
|
80
|
+
const cacheRead = usage.cache_read_input_tokens || 0;
|
|
81
|
+
const cacheCreate = usage.cache_creation_input_tokens || 0;
|
|
82
|
+
const output = usage.output_tokens || 0;
|
|
83
|
+
return input + cacheRead + cacheCreate + output;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read the tail of a JSONL file efficiently, returning up to `maxLines`
|
|
87
|
+
* trailing lines as parsed JSON objects. Lines that fail to parse are skipped.
|
|
88
|
+
*/
|
|
89
|
+
function readTailLines(filePath, maxLines) {
|
|
90
|
+
const stat = fs_1.default.statSync(filePath);
|
|
91
|
+
// Read up to ~256KB from the end — enough for ~hundreds of small entries.
|
|
92
|
+
const readBytes = Math.min(stat.size, 262144);
|
|
93
|
+
const fd = fs_1.default.openSync(filePath, 'r');
|
|
94
|
+
try {
|
|
95
|
+
const buf = Buffer.alloc(readBytes);
|
|
96
|
+
fs_1.default.readSync(fd, buf, 0, readBytes, stat.size - readBytes);
|
|
97
|
+
const text = buf.toString('utf8');
|
|
98
|
+
// Drop any partial first line if we didn't read from byte 0.
|
|
99
|
+
const lines = text.split('\n');
|
|
100
|
+
if (stat.size > readBytes)
|
|
101
|
+
lines.shift();
|
|
102
|
+
const tail = lines.slice(-maxLines).filter(l => l.trim().length > 0);
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const line of tail) {
|
|
105
|
+
try {
|
|
106
|
+
out.push(JSON.parse(line));
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
fs_1.default.closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Extract plain text from a Claude API `content` array, ignoring tool_use blocks.
|
|
118
|
+
*/
|
|
119
|
+
function extractAssistantText(message) {
|
|
120
|
+
const content = message.content;
|
|
121
|
+
if (typeof content === 'string')
|
|
122
|
+
return content;
|
|
123
|
+
if (!Array.isArray(content))
|
|
124
|
+
return '';
|
|
125
|
+
const parts = [];
|
|
126
|
+
for (const block of content) {
|
|
127
|
+
if (block && typeof block === 'object' && block.type === 'text') {
|
|
128
|
+
const text = block.text;
|
|
129
|
+
if (typeof text === 'string')
|
|
130
|
+
parts.push(text);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return parts.join('\n').trim();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Read the latest status info from a session's transcript.
|
|
137
|
+
* Returns null when the transcript file doesn't exist or is unreadable.
|
|
138
|
+
*
|
|
139
|
+
* `maxAssistantChars` truncates the last-message snippet to keep Telegram
|
|
140
|
+
* messages under their 4096-char limit.
|
|
141
|
+
*/
|
|
142
|
+
function readTranscriptStatus(cwd, sessionId, maxAssistantChars = 600) {
|
|
143
|
+
let filePath;
|
|
144
|
+
let resolvedSessionId;
|
|
145
|
+
if (sessionId) {
|
|
146
|
+
filePath = getTranscriptPath(cwd, sessionId);
|
|
147
|
+
resolvedSessionId = sessionId;
|
|
148
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
149
|
+
// Fall through to the latest-transcript fallback when the named session
|
|
150
|
+
// is gone (e.g. user resumed and the bot has stale state).
|
|
151
|
+
const latest = findLatestTranscript(cwd);
|
|
152
|
+
if (!latest)
|
|
153
|
+
return null;
|
|
154
|
+
filePath = latest.path;
|
|
155
|
+
resolvedSessionId = latest.sessionId;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const latest = findLatestTranscript(cwd);
|
|
160
|
+
if (!latest)
|
|
161
|
+
return null;
|
|
162
|
+
filePath = latest.path;
|
|
163
|
+
resolvedSessionId = latest.sessionId;
|
|
164
|
+
}
|
|
165
|
+
let entries;
|
|
166
|
+
try {
|
|
167
|
+
entries = readTailLines(filePath, 100);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (entries.length === 0)
|
|
173
|
+
return null;
|
|
174
|
+
const status = { sessionId: resolvedSessionId };
|
|
175
|
+
// Walk backward to find the most recent assistant entry — that's where model
|
|
176
|
+
// / usage / metadata live. Static metadata (cwd, version, gitBranch, slug)
|
|
177
|
+
// is on every entry, so the last one is fine.
|
|
178
|
+
const last = entries[entries.length - 1];
|
|
179
|
+
if (typeof last.cwd === 'string')
|
|
180
|
+
status.cwd = last.cwd;
|
|
181
|
+
if (typeof last.version === 'string')
|
|
182
|
+
status.version = last.version;
|
|
183
|
+
if (typeof last.gitBranch === 'string')
|
|
184
|
+
status.gitBranch = last.gitBranch;
|
|
185
|
+
if (typeof last.slug === 'string')
|
|
186
|
+
status.slug = last.slug;
|
|
187
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
188
|
+
const entry = entries[i];
|
|
189
|
+
if (entry.type !== 'assistant')
|
|
190
|
+
continue;
|
|
191
|
+
const message = entry.message || {};
|
|
192
|
+
if (!status.model && typeof message.model === 'string') {
|
|
193
|
+
status.model = message.model;
|
|
194
|
+
}
|
|
195
|
+
if (status.contextTokens === undefined) {
|
|
196
|
+
const usage = message.usage;
|
|
197
|
+
if (usage) {
|
|
198
|
+
status.contextTokens = sumContextTokens(usage);
|
|
199
|
+
// 1h ephemeral cache is only available in extended-context (1M) mode,
|
|
200
|
+
// so its presence is a reliable signal that the limit is 1M.
|
|
201
|
+
const cacheCreation = usage.cache_creation;
|
|
202
|
+
const has1hCache = cacheCreation && cacheCreation.ephemeral_1h_input_tokens > 0;
|
|
203
|
+
status.contextLimit = has1hCache ? 1000000 : inferContextLimit(status.model);
|
|
204
|
+
if (status.contextLimit && status.contextLimit > 0) {
|
|
205
|
+
// Don't cap — if tokens exceed the assumed limit, the user is on a
|
|
206
|
+
// larger-context mode we couldn't detect; showing >100% is more honest
|
|
207
|
+
// than silently flooring the value.
|
|
208
|
+
status.contextPct = Math.round((status.contextTokens / status.contextLimit) * 100);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (status.lastAssistantMessage === undefined) {
|
|
213
|
+
const text = extractAssistantText(message);
|
|
214
|
+
if (text.length > 0) {
|
|
215
|
+
status.lastAssistantMessage = text.length > maxAssistantChars
|
|
216
|
+
? text.slice(0, maxAssistantChars).trimEnd() + '…'
|
|
217
|
+
: text;
|
|
218
|
+
if (typeof entry.timestamp === 'string') {
|
|
219
|
+
status.lastAssistantTimestamp = entry.timestamp;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (status.model && status.contextTokens !== undefined && status.lastAssistantMessage !== undefined) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return status;
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=transcript-reader.js.map
|