@ekkos/cli 0.2.7 → 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.
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Stream capture types for mid-turn context preservation
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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 {
@@ -510,9 +511,55 @@ async function run(options) {
510
511
  (0, state_1.clearAutoClearFlag)();
511
512
  // Track state
512
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
+ }
513
528
  let isAutoClearInProgress = false;
514
529
  let transcriptPath = null;
515
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
+ }
516
563
  // ══════════════════════════════════════════════════════════════════════════
517
564
  // SESSION NAME TRACKING (from live TUI output)
518
565
  // Claude prints: "· Turn N · groovy-koala-saves · 📅"
@@ -701,16 +748,26 @@ async function run(options) {
701
748
  }
702
749
  }
703
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
704
753
  if (!sessionToRestore) {
705
754
  const state = (0, state_1.getState)();
706
- sessionToRestore = state?.sessionName || null;
755
+ const persistedName = state?.sessionName || null;
707
756
  const sessionId = state?.sessionId || null;
708
- // If still no session name but we have an ID, generate it
709
- if (!sessionToRestore && sessionId) {
710
- sessionToRestore = (0, state_1.uuidToWords)(sessionId);
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}`);
711
761
  }
712
- if (sessionToRestore) {
713
- dlog(`Using persisted state (last resort): ${sessionToRestore}`);
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
+ }
714
771
  }
715
772
  }
716
773
  const sessionDisplay = sessionToRestore || 'unknown-session';
@@ -796,18 +853,35 @@ async function run(options) {
796
853
  outputBuffer = outputBuffer.slice(-2000);
797
854
  }
798
855
  // Try to extract transcript path from output (Claude shows it on startup)
799
- const transcriptMatch = data.match(/transcript[_\s]?(?:path)?[:\s]+([^\s\n]+\.json)/i);
856
+ const transcriptMatch = data.match(/transcript[_\s]?(?:path)?[:\s]+([^\s\n]+\.jsonl?)/i);
800
857
  if (transcriptMatch) {
801
858
  transcriptPath = transcriptMatch[1];
802
859
  dlog(`Detected transcript: ${transcriptPath}`);
860
+ // Start tailer if we have session ID
861
+ if (currentSessionId && transcriptPath) {
862
+ startStreamTailer(transcriptPath, currentSessionId, currentSession || undefined);
863
+ }
803
864
  }
804
865
  // Try to extract session ID from output (fallback - Claude rarely prints this)
805
866
  const sessionMatch = data.match(/session[_\s]?(?:id)?[:\s]+([a-f0-9-]{36})/i);
806
867
  if (sessionMatch) {
807
868
  currentSessionId = sessionMatch[1];
808
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
809
873
  (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
810
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
+ }
811
885
  }
812
886
  // ════════════════════════════════════════════════════════════════════════
813
887
  // SESSION NAME DETECTION (PRIMARY METHOD)
@@ -829,8 +903,37 @@ async function run(options) {
829
903
  lastSeenSessionAt = Date.now();
830
904
  currentSession = lastSeenSessionName;
831
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
832
909
  (0, state_1.updateState)({ sessionName: currentSession });
833
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
+ }
834
937
  }
835
938
  else {
836
939
  // Same session, just update timestamp
@@ -851,6 +954,9 @@ async function run(options) {
851
954
  lastSeenSessionAt = Date.now();
852
955
  currentSession = lastSeenSessionName;
853
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
854
960
  (0, state_1.updateState)({ sessionName: currentSession });
855
961
  dlog(`Session detected from generic match: ${currentSession} (observedSessionThisRun=true)`);
856
962
  }
@@ -874,6 +980,8 @@ async function run(options) {
874
980
  // Handle PTY exit
875
981
  shell.onExit(({ exitCode }) => {
876
982
  (0, state_1.clearAutoClearFlag)();
983
+ stopStreamTailer(); // Stop stream capture
984
+ (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
877
985
  // Restore terminal
878
986
  if (process.stdin.isTTY) {
879
987
  process.stdin.setRawMode(false);
@@ -886,6 +994,8 @@ async function run(options) {
886
994
  // Cleanup on exit signals
887
995
  const cleanup = () => {
888
996
  (0, state_1.clearAutoClearFlag)();
997
+ stopStreamTailer(); // Stop stream capture
998
+ (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
889
999
  if (process.stdin.isTTY) {
890
1000
  process.stdin.setRawMode(false);
891
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 {};