@ekkos/cli 0.2.6 → 0.2.8
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/dist/cache/types.d.ts +11 -2
- package/dist/capture/index.d.ts +8 -0
- package/dist/capture/index.js +24 -0
- package/dist/capture/stream-tailer.d.ts +138 -0
- package/dist/capture/stream-tailer.js +658 -0
- package/dist/capture/types.d.ts +227 -0
- package/dist/capture/types.js +5 -0
- package/dist/commands/run.js +147 -10
- package/dist/commands/stream.d.ts +19 -0
- package/dist/commands/stream.js +340 -0
- package/dist/index.js +58 -1
- package/dist/restore/RestoreOrchestrator.d.ts +6 -0
- package/dist/restore/RestoreOrchestrator.js +174 -22
- package/dist/utils/state.d.ts +39 -1
- package/dist/utils/state.js +152 -2
- package/package.json +1 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream capture types for mid-turn context preservation
|
|
3
|
+
*/
|
|
4
|
+
export type TurnStatus = 'in_progress' | 'complete';
|
|
5
|
+
/**
|
|
6
|
+
* How a turn was sealed (closed)
|
|
7
|
+
*/
|
|
8
|
+
export type SealReason = 'user_boundary' | 'explicit' | 'process_exit' | 'context_wall';
|
|
9
|
+
export interface ToolEvent {
|
|
10
|
+
kind: 'tool_use' | 'tool_result';
|
|
11
|
+
id: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
input?: unknown;
|
|
14
|
+
output?: unknown;
|
|
15
|
+
is_error?: boolean;
|
|
16
|
+
ts: string;
|
|
17
|
+
transcript_line_uuid?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface StreamTurn {
|
|
20
|
+
session_id: string;
|
|
21
|
+
session_name?: string;
|
|
22
|
+
turn_id: number;
|
|
23
|
+
started_at: string;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
user_query?: string;
|
|
26
|
+
assistant_stream: string;
|
|
27
|
+
assistant_response?: string;
|
|
28
|
+
status: TurnStatus;
|
|
29
|
+
stream_offset: number;
|
|
30
|
+
tools: ToolEvent[];
|
|
31
|
+
files_referenced?: string[];
|
|
32
|
+
files_modified?: string[];
|
|
33
|
+
last_text_delta_ts?: string;
|
|
34
|
+
last_tool_event_ts?: string;
|
|
35
|
+
sealed_at?: string;
|
|
36
|
+
sealed_by?: SealReason;
|
|
37
|
+
}
|
|
38
|
+
export type StreamEvent = {
|
|
39
|
+
kind: 'session_start';
|
|
40
|
+
event_id: string;
|
|
41
|
+
session_id: string;
|
|
42
|
+
session_name?: string;
|
|
43
|
+
ts: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'user_query';
|
|
46
|
+
event_id: string;
|
|
47
|
+
turn_id: number;
|
|
48
|
+
query: string;
|
|
49
|
+
ts: string;
|
|
50
|
+
transcript_line_uuid?: string;
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'assistant_text_delta';
|
|
53
|
+
event_id: string;
|
|
54
|
+
turn_id: number;
|
|
55
|
+
delta: string;
|
|
56
|
+
offset: number;
|
|
57
|
+
ts: string;
|
|
58
|
+
transcript_line_uuid?: string;
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'tool_use';
|
|
61
|
+
event_id: string;
|
|
62
|
+
turn_id: number;
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
input?: unknown;
|
|
66
|
+
ts: string;
|
|
67
|
+
transcript_line_uuid?: string;
|
|
68
|
+
} | {
|
|
69
|
+
kind: 'tool_result';
|
|
70
|
+
event_id: string;
|
|
71
|
+
turn_id: number;
|
|
72
|
+
id: string;
|
|
73
|
+
output?: unknown;
|
|
74
|
+
is_error?: boolean;
|
|
75
|
+
ts: string;
|
|
76
|
+
transcript_line_uuid?: string;
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'seal_turn';
|
|
79
|
+
event_id: string;
|
|
80
|
+
turn_id: number;
|
|
81
|
+
reason: SealReason;
|
|
82
|
+
ts: string;
|
|
83
|
+
};
|
|
84
|
+
export interface TranscriptLine {
|
|
85
|
+
type: 'user' | 'assistant' | string;
|
|
86
|
+
uuid: string;
|
|
87
|
+
parentUuid?: string | null;
|
|
88
|
+
timestamp: string;
|
|
89
|
+
sessionId: string;
|
|
90
|
+
cwd?: string;
|
|
91
|
+
gitBranch?: string;
|
|
92
|
+
version?: string;
|
|
93
|
+
isMeta?: boolean;
|
|
94
|
+
message: {
|
|
95
|
+
role: 'user' | 'assistant';
|
|
96
|
+
model?: string;
|
|
97
|
+
content: string | ContentBlock[];
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export interface ContentBlock {
|
|
101
|
+
type: 'text' | 'thinking' | 'tool_use' | 'tool_result';
|
|
102
|
+
text?: string;
|
|
103
|
+
thinking?: string;
|
|
104
|
+
signature?: string;
|
|
105
|
+
id?: string;
|
|
106
|
+
name?: string;
|
|
107
|
+
input?: unknown;
|
|
108
|
+
tool_use_id?: string;
|
|
109
|
+
content?: unknown;
|
|
110
|
+
is_error?: boolean;
|
|
111
|
+
}
|
|
112
|
+
export interface TailerState {
|
|
113
|
+
transcriptPath: string;
|
|
114
|
+
sessionId: string;
|
|
115
|
+
sessionName?: string;
|
|
116
|
+
readOffset: number;
|
|
117
|
+
pendingBuffer: string;
|
|
118
|
+
currentTurnId: number;
|
|
119
|
+
currentTurnStatus: TurnStatus;
|
|
120
|
+
lastActivityTs: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Stream state - where are we in the conversation flow?
|
|
124
|
+
*/
|
|
125
|
+
export type StreamState = 'idle' | 'assistant_streaming' | 'tool_running' | 'interrupted' | 'wall_hit';
|
|
126
|
+
/**
|
|
127
|
+
* Stream state machine - deterministic resume anchor
|
|
128
|
+
*/
|
|
129
|
+
export interface StreamStateMachine {
|
|
130
|
+
session_id: string;
|
|
131
|
+
session_name?: string;
|
|
132
|
+
state: StreamState;
|
|
133
|
+
state_entered_at: string;
|
|
134
|
+
last_complete_turn_id: number;
|
|
135
|
+
current_turn_id: number;
|
|
136
|
+
in_progress_text_head: string;
|
|
137
|
+
in_progress_text_tail: string;
|
|
138
|
+
in_progress_total_chars: number;
|
|
139
|
+
open_loops: Array<{
|
|
140
|
+
type: 'tool' | 'plan_step';
|
|
141
|
+
id: string;
|
|
142
|
+
name: string;
|
|
143
|
+
started_at: string;
|
|
144
|
+
}>;
|
|
145
|
+
last_event_ts: string;
|
|
146
|
+
last_checkpoint_ts: string;
|
|
147
|
+
stream_bytes_captured: number;
|
|
148
|
+
events_captured: number;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* In-progress checkpoint - saved every N seconds for crash recovery
|
|
152
|
+
*/
|
|
153
|
+
export interface StreamCheckpoint {
|
|
154
|
+
session_id: string;
|
|
155
|
+
turn_id: number;
|
|
156
|
+
checkpoint_ts: string;
|
|
157
|
+
text_head: string;
|
|
158
|
+
text_tail: string;
|
|
159
|
+
state: StreamState;
|
|
160
|
+
open_loops: Array<{
|
|
161
|
+
type: string;
|
|
162
|
+
id: string;
|
|
163
|
+
name: string;
|
|
164
|
+
started_at: string;
|
|
165
|
+
}>;
|
|
166
|
+
total_chars: number;
|
|
167
|
+
events_since_last_checkpoint: number;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Open loop - work started but not completed
|
|
171
|
+
*/
|
|
172
|
+
export interface OpenLoop {
|
|
173
|
+
type: 'tool' | 'plan_step' | 'user_request';
|
|
174
|
+
name: string;
|
|
175
|
+
status: 'in_progress' | 'blocked' | 'waiting';
|
|
176
|
+
started_at: string;
|
|
177
|
+
context?: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Work phase derived from activity patterns
|
|
181
|
+
*/
|
|
182
|
+
export type WorkPhase = 'Plan' | 'Implement' | 'Verify' | 'Fix' | 'Deploy';
|
|
183
|
+
/**
|
|
184
|
+
* WorkState Capsule - deterministic resume anchor for Time Machine
|
|
185
|
+
*
|
|
186
|
+
* This is the machine-derived state that guarantees perfect continuation.
|
|
187
|
+
* It's built from streaming capture data and injected by /continue.
|
|
188
|
+
*/
|
|
189
|
+
export interface WorkStateCapsule {
|
|
190
|
+
goal: string;
|
|
191
|
+
phase: WorkPhase;
|
|
192
|
+
next_step: string;
|
|
193
|
+
open_loops: OpenLoop[];
|
|
194
|
+
files_in_play: string[];
|
|
195
|
+
decisions_made: string[];
|
|
196
|
+
blockers?: string[];
|
|
197
|
+
last_good_output_tail: string;
|
|
198
|
+
continuation_instruction: string;
|
|
199
|
+
generated_at: string;
|
|
200
|
+
turn_id: number;
|
|
201
|
+
session_id: string;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Timeline event for Time Machine display
|
|
205
|
+
*/
|
|
206
|
+
export interface TimelineEvent {
|
|
207
|
+
turn_id: number;
|
|
208
|
+
timestamp: string;
|
|
209
|
+
phase: WorkPhase;
|
|
210
|
+
summary: string;
|
|
211
|
+
impact?: string;
|
|
212
|
+
tools_used: string[];
|
|
213
|
+
files_touched: string[];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Evidence entry for audit trail
|
|
217
|
+
*/
|
|
218
|
+
export interface EvidenceEntry {
|
|
219
|
+
turn_id: number;
|
|
220
|
+
timestamp: string;
|
|
221
|
+
tool_events: ToolEvent[];
|
|
222
|
+
files_touched: string[];
|
|
223
|
+
diff_summary?: string;
|
|
224
|
+
transcript_line_uuid: string;
|
|
225
|
+
transcript_path: string;
|
|
226
|
+
raw_transcript_offset?: number;
|
|
227
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -44,6 +44,7 @@ const os = __importStar(require("os"));
|
|
|
44
44
|
const child_process_1 = require("child_process");
|
|
45
45
|
const state_1 = require("../utils/state");
|
|
46
46
|
const doctor_1 = require("./doctor");
|
|
47
|
+
const stream_tailer_1 = require("../capture/stream-tailer");
|
|
47
48
|
// Try to load node-pty (may fail on Node 24+)
|
|
48
49
|
let pty = null;
|
|
49
50
|
try {
|
|
@@ -107,6 +108,25 @@ const SESSION_NAME_IN_STATUS_REGEX = /·\s*([a-z]+-[a-z]+-[a-z]+)\s*·/i;
|
|
|
107
108
|
// Weaker signal: any 3-word slug (word-word-word pattern)
|
|
108
109
|
const SESSION_NAME_REGEX = /\b([a-z]+-[a-z]+-[a-z]+)\b/i;
|
|
109
110
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
111
|
+
// SESSION NAME VALIDATION (MUST use words from session-words.json)
|
|
112
|
+
// This is the SOURCE OF TRUTH for valid session names
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
const session_words_json_1 = __importDefault(require("../utils/session-words.json"));
|
|
115
|
+
const VALID_ADJECTIVES = new Set(session_words_json_1.default.adjectives.map((w) => w.toLowerCase()));
|
|
116
|
+
const VALID_NOUNS = new Set(session_words_json_1.default.nouns.map((w) => w.toLowerCase()));
|
|
117
|
+
const VALID_VERBS = new Set(session_words_json_1.default.verbs.map((w) => w.toLowerCase()));
|
|
118
|
+
/**
|
|
119
|
+
* Validate that a detected session name actually uses words from our word lists.
|
|
120
|
+
* This prevents false positives like "x-github-event" from being detected.
|
|
121
|
+
*/
|
|
122
|
+
function isValidSessionName(name) {
|
|
123
|
+
const parts = name.toLowerCase().split('-');
|
|
124
|
+
if (parts.length !== 3)
|
|
125
|
+
return false;
|
|
126
|
+
const [adj, noun, verb] = parts;
|
|
127
|
+
return VALID_ADJECTIVES.has(adj) && VALID_NOUNS.has(noun) && VALID_VERBS.has(verb);
|
|
128
|
+
}
|
|
129
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
110
130
|
// LOGGING (FILE ONLY DURING TUI - NO TERMINAL CORRUPTION)
|
|
111
131
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
112
132
|
let _debugLogPath = path.join(os.homedir(), '.ekkos', 'auto-continue.debug.log');
|
|
@@ -491,9 +511,55 @@ async function run(options) {
|
|
|
491
511
|
(0, state_1.clearAutoClearFlag)();
|
|
492
512
|
// Track state
|
|
493
513
|
let currentSession = options.session || (0, state_1.getCurrentSessionName)();
|
|
514
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
515
|
+
// MULTI-SESSION SUPPORT: Register this process as an active session
|
|
516
|
+
// This prevents state collision when multiple Claude Code instances run
|
|
517
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
518
|
+
(0, state_1.registerActiveSession)('pending', // Session ID not yet known
|
|
519
|
+
currentSession || 'initializing', process.cwd());
|
|
520
|
+
dlog(`Registered active session (PID ${process.pid})`);
|
|
521
|
+
// Show active sessions count if verbose
|
|
522
|
+
if (verbose) {
|
|
523
|
+
const activeSessions = (0, state_1.getActiveSessions)();
|
|
524
|
+
if (activeSessions.length > 1) {
|
|
525
|
+
console.log(chalk_1.default.cyan(` 📊 Active ekkOS sessions: ${activeSessions.length}`));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
494
528
|
let isAutoClearInProgress = false;
|
|
495
529
|
let transcriptPath = null;
|
|
496
530
|
let currentSessionId = null;
|
|
531
|
+
// Stream tailer for mid-turn context capture
|
|
532
|
+
let streamTailer = null;
|
|
533
|
+
const streamCacheDir = path.join(os.homedir(), '.ekkos', 'cache', 'sessions');
|
|
534
|
+
// Helper to start stream tailer when we have transcript path
|
|
535
|
+
function startStreamTailer(tPath, sId, sName) {
|
|
536
|
+
if (streamTailer)
|
|
537
|
+
return; // Already running
|
|
538
|
+
try {
|
|
539
|
+
streamTailer = new stream_tailer_1.StreamTailer({
|
|
540
|
+
transcriptPath: tPath,
|
|
541
|
+
sessionId: sId,
|
|
542
|
+
sessionName: sName,
|
|
543
|
+
cacheDir: streamCacheDir,
|
|
544
|
+
onEvent: (event) => {
|
|
545
|
+
dlog(`Stream event: ${event.kind} (turn ${event.turn_id || 'n/a'})`);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
streamTailer.start();
|
|
549
|
+
dlog(`Stream tailer started for ${sName || sId}`);
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
dlog(`Failed to start stream tailer: ${err.message}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Helper to stop stream tailer
|
|
556
|
+
function stopStreamTailer() {
|
|
557
|
+
if (streamTailer) {
|
|
558
|
+
streamTailer.stop();
|
|
559
|
+
streamTailer = null;
|
|
560
|
+
dlog('Stream tailer stopped');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
497
563
|
// ══════════════════════════════════════════════════════════════════════════
|
|
498
564
|
// SESSION NAME TRACKING (from live TUI output)
|
|
499
565
|
// Claude prints: "· Turn N · groovy-koala-saves · 📅"
|
|
@@ -682,16 +748,26 @@ async function run(options) {
|
|
|
682
748
|
}
|
|
683
749
|
}
|
|
684
750
|
// PRIORITY 4: Persisted state (last resort)
|
|
751
|
+
// CRITICAL: Always validate sessionName - persisted state may have invalid names
|
|
752
|
+
// like "claude-plugins-official" from directory names or false positives
|
|
685
753
|
if (!sessionToRestore) {
|
|
686
754
|
const state = (0, state_1.getState)();
|
|
687
|
-
|
|
755
|
+
const persistedName = state?.sessionName || null;
|
|
688
756
|
const sessionId = state?.sessionId || null;
|
|
689
|
-
//
|
|
690
|
-
if (
|
|
691
|
-
sessionToRestore =
|
|
757
|
+
// Validate persisted name - only use if it passes word list validation
|
|
758
|
+
if (persistedName && isValidSessionName(persistedName)) {
|
|
759
|
+
sessionToRestore = persistedName;
|
|
760
|
+
dlog(`Using validated persisted state: ${sessionToRestore}`);
|
|
692
761
|
}
|
|
693
|
-
if (
|
|
694
|
-
|
|
762
|
+
else if (sessionId) {
|
|
763
|
+
// Derive from UUID if persisted name is invalid or missing
|
|
764
|
+
sessionToRestore = (0, state_1.uuidToWords)(sessionId);
|
|
765
|
+
if (persistedName) {
|
|
766
|
+
dlog(`Persisted name "${persistedName}" invalid, derived from UUID: ${sessionToRestore}`);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
dlog(`No persisted name, derived from UUID: ${sessionToRestore}`);
|
|
770
|
+
}
|
|
695
771
|
}
|
|
696
772
|
}
|
|
697
773
|
const sessionDisplay = sessionToRestore || 'unknown-session';
|
|
@@ -777,18 +853,35 @@ async function run(options) {
|
|
|
777
853
|
outputBuffer = outputBuffer.slice(-2000);
|
|
778
854
|
}
|
|
779
855
|
// Try to extract transcript path from output (Claude shows it on startup)
|
|
780
|
-
const transcriptMatch = data.match(/transcript[_\s]?(?:path)?[:\s]+([^\s\n]+\.
|
|
856
|
+
const transcriptMatch = data.match(/transcript[_\s]?(?:path)?[:\s]+([^\s\n]+\.jsonl?)/i);
|
|
781
857
|
if (transcriptMatch) {
|
|
782
858
|
transcriptPath = transcriptMatch[1];
|
|
783
859
|
dlog(`Detected transcript: ${transcriptPath}`);
|
|
860
|
+
// Start tailer if we have session ID
|
|
861
|
+
if (currentSessionId && transcriptPath) {
|
|
862
|
+
startStreamTailer(transcriptPath, currentSessionId, currentSession || undefined);
|
|
863
|
+
}
|
|
784
864
|
}
|
|
785
865
|
// Try to extract session ID from output (fallback - Claude rarely prints this)
|
|
786
866
|
const sessionMatch = data.match(/session[_\s]?(?:id)?[:\s]+([a-f0-9-]{36})/i);
|
|
787
867
|
if (sessionMatch) {
|
|
788
868
|
currentSessionId = sessionMatch[1];
|
|
789
869
|
currentSession = (0, state_1.uuidToWords)(currentSessionId);
|
|
870
|
+
// Update THIS process's session entry (not global state.json)
|
|
871
|
+
(0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession);
|
|
872
|
+
// Also update global state for backwards compatibility
|
|
790
873
|
(0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
|
|
791
874
|
dlog(`Session detected from UUID: ${currentSession}`);
|
|
875
|
+
// Try to find/construct transcript path from session ID
|
|
876
|
+
if (!transcriptPath) {
|
|
877
|
+
const encodedCwd = process.cwd().replace(/\//g, '-').replace(/^-/, '');
|
|
878
|
+
const possibleTranscript = path.join(os.homedir(), '.claude', 'projects', encodedCwd, `${currentSessionId}.jsonl`);
|
|
879
|
+
if (fs.existsSync(possibleTranscript)) {
|
|
880
|
+
transcriptPath = possibleTranscript;
|
|
881
|
+
dlog(`Found transcript from session ID: ${transcriptPath}`);
|
|
882
|
+
startStreamTailer(transcriptPath, currentSessionId, currentSession || undefined);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
792
885
|
}
|
|
793
886
|
// ════════════════════════════════════════════════════════════════════════
|
|
794
887
|
// SESSION NAME DETECTION (PRIMARY METHOD)
|
|
@@ -800,14 +893,47 @@ async function run(options) {
|
|
|
800
893
|
const statusMatch = plain.match(SESSION_NAME_IN_STATUS_REGEX);
|
|
801
894
|
if (statusMatch) {
|
|
802
895
|
const detectedSession = statusMatch[1].toLowerCase();
|
|
803
|
-
//
|
|
804
|
-
if (detectedSession
|
|
896
|
+
// Validate against word lists (SOURCE OF TRUTH)
|
|
897
|
+
if (!isValidSessionName(detectedSession)) {
|
|
898
|
+
dlog(`Session rejected (invalid words): ${detectedSession}`);
|
|
899
|
+
}
|
|
900
|
+
else if (detectedSession !== lastSeenSessionName) {
|
|
901
|
+
// Only update if different (avoid log spam)
|
|
805
902
|
lastSeenSessionName = detectedSession;
|
|
806
903
|
lastSeenSessionAt = Date.now();
|
|
807
904
|
currentSession = lastSeenSessionName;
|
|
808
905
|
observedSessionThisRun = true; // Mark that we've seen a session in THIS process
|
|
906
|
+
// Update THIS process's session entry (not global state.json)
|
|
907
|
+
(0, state_1.updateCurrentProcessSession)(currentSessionId || 'unknown', currentSession);
|
|
908
|
+
// Also update global state for backwards compatibility
|
|
809
909
|
(0, state_1.updateState)({ sessionName: currentSession });
|
|
810
910
|
dlog(`Session detected from status line: ${currentSession} (observedSessionThisRun=true)`);
|
|
911
|
+
// Try to start stream tailer - scan for matching transcript file
|
|
912
|
+
if (!streamTailer) {
|
|
913
|
+
const encodedCwd = process.cwd().replace(/\//g, '-').replace(/^-/, '');
|
|
914
|
+
const projectDir = path.join(os.homedir(), '.claude', 'projects', encodedCwd);
|
|
915
|
+
try {
|
|
916
|
+
const files = fs.readdirSync(projectDir);
|
|
917
|
+
// Find most recent .jsonl file (likely current session)
|
|
918
|
+
const jsonlFiles = files
|
|
919
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
920
|
+
.map(f => ({
|
|
921
|
+
name: f,
|
|
922
|
+
path: path.join(projectDir, f),
|
|
923
|
+
mtime: fs.statSync(path.join(projectDir, f)).mtimeMs
|
|
924
|
+
}))
|
|
925
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
926
|
+
if (jsonlFiles.length > 0) {
|
|
927
|
+
transcriptPath = jsonlFiles[0].path;
|
|
928
|
+
currentSessionId = jsonlFiles[0].name.replace('.jsonl', '');
|
|
929
|
+
dlog(`Found transcript from project scan: ${transcriptPath}`);
|
|
930
|
+
startStreamTailer(transcriptPath, currentSessionId, currentSession);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
// Project dir might not exist yet
|
|
935
|
+
}
|
|
936
|
+
}
|
|
811
937
|
}
|
|
812
938
|
else {
|
|
813
939
|
// Same session, just update timestamp
|
|
@@ -819,11 +945,18 @@ async function run(options) {
|
|
|
819
945
|
const anyMatch = plain.match(SESSION_NAME_REGEX);
|
|
820
946
|
if (anyMatch) {
|
|
821
947
|
const detectedSession = anyMatch[1].toLowerCase();
|
|
822
|
-
|
|
948
|
+
// Validate against word lists (SOURCE OF TRUTH)
|
|
949
|
+
if (!isValidSessionName(detectedSession)) {
|
|
950
|
+
dlog(`Session rejected (invalid words): ${detectedSession}`);
|
|
951
|
+
}
|
|
952
|
+
else if (detectedSession !== lastSeenSessionName) {
|
|
823
953
|
lastSeenSessionName = detectedSession;
|
|
824
954
|
lastSeenSessionAt = Date.now();
|
|
825
955
|
currentSession = lastSeenSessionName;
|
|
826
956
|
observedSessionThisRun = true; // Mark that we've seen a session in THIS process
|
|
957
|
+
// Update THIS process's session entry (not global state.json)
|
|
958
|
+
(0, state_1.updateCurrentProcessSession)(currentSessionId || 'unknown', currentSession);
|
|
959
|
+
// Also update global state for backwards compatibility
|
|
827
960
|
(0, state_1.updateState)({ sessionName: currentSession });
|
|
828
961
|
dlog(`Session detected from generic match: ${currentSession} (observedSessionThisRun=true)`);
|
|
829
962
|
}
|
|
@@ -847,6 +980,8 @@ async function run(options) {
|
|
|
847
980
|
// Handle PTY exit
|
|
848
981
|
shell.onExit(({ exitCode }) => {
|
|
849
982
|
(0, state_1.clearAutoClearFlag)();
|
|
983
|
+
stopStreamTailer(); // Stop stream capture
|
|
984
|
+
(0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
|
|
850
985
|
// Restore terminal
|
|
851
986
|
if (process.stdin.isTTY) {
|
|
852
987
|
process.stdin.setRawMode(false);
|
|
@@ -859,6 +994,8 @@ async function run(options) {
|
|
|
859
994
|
// Cleanup on exit signals
|
|
860
995
|
const cleanup = () => {
|
|
861
996
|
(0, state_1.clearAutoClearFlag)();
|
|
997
|
+
stopStreamTailer(); // Stop stream capture
|
|
998
|
+
(0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
|
|
862
999
|
if (process.stdin.isTTY) {
|
|
863
1000
|
process.stdin.setRawMode(false);
|
|
864
1001
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ekkos stream - Stream capture status and management
|
|
3
|
+
*
|
|
4
|
+
* Shows live status of stream capture for debugging and support.
|
|
5
|
+
*/
|
|
6
|
+
interface StreamStatusOptions {
|
|
7
|
+
session?: string;
|
|
8
|
+
watch?: boolean;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Display stream status
|
|
13
|
+
*/
|
|
14
|
+
export declare function streamStatus(options: StreamStatusOptions): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* List all sessions with stream data
|
|
17
|
+
*/
|
|
18
|
+
export declare function streamList(): void;
|
|
19
|
+
export {};
|