@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.
- 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 +117 -7
- 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,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ekkos stream - Stream capture status and management
|
|
4
|
+
*
|
|
5
|
+
* Shows live status of stream capture for debugging and support.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
41
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.streamStatus = streamStatus;
|
|
45
|
+
exports.streamList = streamList;
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const os = __importStar(require("os"));
|
|
50
|
+
const STREAM_CACHE_DIR = path.join(os.homedir(), '.ekkos', 'cache', 'sessions');
|
|
51
|
+
/**
|
|
52
|
+
* Get the state machine file path for a session
|
|
53
|
+
*/
|
|
54
|
+
function getStateMachinePath(sessionId) {
|
|
55
|
+
return path.join(STREAM_CACHE_DIR, `${sessionId}.state.json`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the checkpoint file path for a session
|
|
59
|
+
*/
|
|
60
|
+
function getCheckpointPath(sessionId) {
|
|
61
|
+
return path.join(STREAM_CACHE_DIR, `${sessionId}.checkpoint.json`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get the stream log path for a session
|
|
65
|
+
*/
|
|
66
|
+
function getStreamLogPath(sessionId) {
|
|
67
|
+
return path.join(STREAM_CACHE_DIR, `${sessionId}.stream.jsonl`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Find most recent session with stream data
|
|
71
|
+
*/
|
|
72
|
+
function findMostRecentSession() {
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(STREAM_CACHE_DIR))
|
|
75
|
+
return null;
|
|
76
|
+
const files = fs.readdirSync(STREAM_CACHE_DIR)
|
|
77
|
+
.filter(f => f.endsWith('.stream.jsonl'))
|
|
78
|
+
.map(f => ({
|
|
79
|
+
sessionId: f.replace('.stream.jsonl', ''),
|
|
80
|
+
mtime: fs.statSync(path.join(STREAM_CACHE_DIR, f)).mtimeMs
|
|
81
|
+
}))
|
|
82
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
83
|
+
return files.length > 0 ? files[0].sessionId : null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Load stream state machine from disk
|
|
91
|
+
*/
|
|
92
|
+
function loadStateMachine(sessionId) {
|
|
93
|
+
try {
|
|
94
|
+
const statePath = getStateMachinePath(sessionId);
|
|
95
|
+
if (fs.existsSync(statePath)) {
|
|
96
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Ignore errors
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Load checkpoint from disk
|
|
106
|
+
*/
|
|
107
|
+
function loadCheckpoint(sessionId) {
|
|
108
|
+
try {
|
|
109
|
+
const checkpointPath = getCheckpointPath(sessionId);
|
|
110
|
+
if (fs.existsSync(checkpointPath)) {
|
|
111
|
+
return JSON.parse(fs.readFileSync(checkpointPath, 'utf-8'));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Ignore errors
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get stream log stats
|
|
121
|
+
*/
|
|
122
|
+
function getStreamLogStats(sessionId) {
|
|
123
|
+
const logPath = getStreamLogPath(sessionId);
|
|
124
|
+
if (!fs.existsSync(logPath)) {
|
|
125
|
+
return { exists: false, size: 0, lines: 0 };
|
|
126
|
+
}
|
|
127
|
+
const stat = fs.statSync(logPath);
|
|
128
|
+
let lines = 0;
|
|
129
|
+
try {
|
|
130
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
131
|
+
lines = content.split('\n').filter(l => l.trim()).length;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Ignore errors
|
|
135
|
+
}
|
|
136
|
+
return { exists: true, size: stat.size, lines };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Format state with color
|
|
140
|
+
*/
|
|
141
|
+
function formatState(state) {
|
|
142
|
+
switch (state) {
|
|
143
|
+
case 'idle':
|
|
144
|
+
return chalk_1.default.gray('● idle');
|
|
145
|
+
case 'assistant_streaming':
|
|
146
|
+
return chalk_1.default.green('◉ streaming');
|
|
147
|
+
case 'tool_running':
|
|
148
|
+
return chalk_1.default.yellow('◉ tool running');
|
|
149
|
+
case 'interrupted':
|
|
150
|
+
return chalk_1.default.red('○ interrupted');
|
|
151
|
+
case 'wall_hit':
|
|
152
|
+
return chalk_1.default.red('✗ wall hit');
|
|
153
|
+
default:
|
|
154
|
+
return chalk_1.default.gray('? unknown');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Format bytes as human-readable
|
|
159
|
+
*/
|
|
160
|
+
function formatBytes(bytes) {
|
|
161
|
+
if (bytes < 1024)
|
|
162
|
+
return `${bytes} B`;
|
|
163
|
+
if (bytes < 1024 * 1024)
|
|
164
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
165
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Format timestamp as relative time
|
|
169
|
+
*/
|
|
170
|
+
function formatRelativeTime(ts) {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const then = new Date(ts).getTime();
|
|
173
|
+
const diff = now - then;
|
|
174
|
+
if (diff < 1000)
|
|
175
|
+
return 'just now';
|
|
176
|
+
if (diff < 60000)
|
|
177
|
+
return `${Math.floor(diff / 1000)}s ago`;
|
|
178
|
+
if (diff < 3600000)
|
|
179
|
+
return `${Math.floor(diff / 60000)}m ago`;
|
|
180
|
+
if (diff < 86400000)
|
|
181
|
+
return `${Math.floor(diff / 3600000)}h ago`;
|
|
182
|
+
return `${Math.floor(diff / 86400000)}d ago`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Display stream status
|
|
186
|
+
*/
|
|
187
|
+
async function streamStatus(options) {
|
|
188
|
+
const sessionId = options.session || findMostRecentSession();
|
|
189
|
+
if (!sessionId) {
|
|
190
|
+
console.log(chalk_1.default.yellow('No active stream sessions found.'));
|
|
191
|
+
console.log(chalk_1.default.gray('Start a session with: ekkos run'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const state = loadStateMachine(sessionId);
|
|
195
|
+
const checkpoint = loadCheckpoint(sessionId);
|
|
196
|
+
const logStats = getStreamLogStats(sessionId);
|
|
197
|
+
if (options.json) {
|
|
198
|
+
console.log(JSON.stringify({
|
|
199
|
+
session_id: sessionId,
|
|
200
|
+
state,
|
|
201
|
+
checkpoint,
|
|
202
|
+
log_stats: logStats,
|
|
203
|
+
}, null, 2));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Header
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(chalk_1.default.cyan.bold('═══════════════════════════════════════════════════════════════'));
|
|
209
|
+
console.log(chalk_1.default.cyan.bold(' ekkOS Stream Status'));
|
|
210
|
+
console.log(chalk_1.default.cyan.bold('═══════════════════════════════════════════════════════════════'));
|
|
211
|
+
console.log('');
|
|
212
|
+
// Session info
|
|
213
|
+
console.log(chalk_1.default.white.bold('Session'));
|
|
214
|
+
console.log(chalk_1.default.gray(` ID: ${sessionId}`));
|
|
215
|
+
if (state?.session_name) {
|
|
216
|
+
console.log(chalk_1.default.gray(` Name: ${state.session_name}`));
|
|
217
|
+
}
|
|
218
|
+
console.log('');
|
|
219
|
+
// State machine
|
|
220
|
+
console.log(chalk_1.default.white.bold('State Machine'));
|
|
221
|
+
if (state) {
|
|
222
|
+
console.log(` State: ${formatState(state.state)}`);
|
|
223
|
+
console.log(chalk_1.default.gray(` Entered: ${formatRelativeTime(state.state_entered_at)}`));
|
|
224
|
+
console.log(chalk_1.default.gray(` Current turn: ${state.current_turn_id}`));
|
|
225
|
+
console.log(chalk_1.default.gray(` Last complete: ${state.last_complete_turn_id}`));
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
console.log(chalk_1.default.gray(' No state machine found'));
|
|
229
|
+
}
|
|
230
|
+
console.log('');
|
|
231
|
+
// In-progress content
|
|
232
|
+
if (state && state.in_progress_total_chars > 0) {
|
|
233
|
+
console.log(chalk_1.default.white.bold('In-Progress Content'));
|
|
234
|
+
console.log(chalk_1.default.gray(` Total chars: ${state.in_progress_total_chars.toLocaleString()}`));
|
|
235
|
+
if (state.in_progress_text_head) {
|
|
236
|
+
console.log(chalk_1.default.gray(` Head (512 chars): "${state.in_progress_text_head.slice(0, 60)}..."`));
|
|
237
|
+
}
|
|
238
|
+
if (state.in_progress_text_tail) {
|
|
239
|
+
const tailPreview = state.in_progress_text_tail.slice(-60);
|
|
240
|
+
console.log(chalk_1.default.gray(` Tail (8KB): "...${tailPreview}"`));
|
|
241
|
+
}
|
|
242
|
+
console.log('');
|
|
243
|
+
}
|
|
244
|
+
// Open loops
|
|
245
|
+
if (state && state.open_loops.length > 0) {
|
|
246
|
+
console.log(chalk_1.default.yellow.bold('⚠️ Open Loops'));
|
|
247
|
+
for (const loop of state.open_loops) {
|
|
248
|
+
console.log(chalk_1.default.yellow(` • ${loop.type}: ${loop.name} (${formatRelativeTime(loop.started_at)})`));
|
|
249
|
+
}
|
|
250
|
+
console.log('');
|
|
251
|
+
}
|
|
252
|
+
// Stream log stats
|
|
253
|
+
console.log(chalk_1.default.white.bold('Stream Log'));
|
|
254
|
+
if (logStats.exists) {
|
|
255
|
+
console.log(chalk_1.default.gray(` Path: ${getStreamLogPath(sessionId)}`));
|
|
256
|
+
console.log(chalk_1.default.gray(` Size: ${formatBytes(logStats.size)}`));
|
|
257
|
+
console.log(chalk_1.default.gray(` Events: ${logStats.lines.toLocaleString()}`));
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.log(chalk_1.default.gray(' No stream log found'));
|
|
261
|
+
}
|
|
262
|
+
console.log('');
|
|
263
|
+
// Checkpoint
|
|
264
|
+
console.log(chalk_1.default.white.bold('Last Checkpoint'));
|
|
265
|
+
if (checkpoint) {
|
|
266
|
+
console.log(chalk_1.default.gray(` Time: ${formatRelativeTime(checkpoint.checkpoint_ts)}`));
|
|
267
|
+
console.log(chalk_1.default.gray(` Turn: ${checkpoint.turn_id}`));
|
|
268
|
+
console.log(chalk_1.default.gray(` Chars: ${checkpoint.total_chars.toLocaleString()}`));
|
|
269
|
+
console.log(chalk_1.default.gray(` Events since: ${checkpoint.events_since_last_checkpoint}`));
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
console.log(chalk_1.default.gray(' No checkpoint found'));
|
|
273
|
+
}
|
|
274
|
+
console.log('');
|
|
275
|
+
// Metrics
|
|
276
|
+
if (state) {
|
|
277
|
+
console.log(chalk_1.default.white.bold('Metrics'));
|
|
278
|
+
console.log(chalk_1.default.gray(` Bytes captured: ${formatBytes(state.stream_bytes_captured)}`));
|
|
279
|
+
console.log(chalk_1.default.gray(` Events captured: ${state.events_captured.toLocaleString()}`));
|
|
280
|
+
console.log(chalk_1.default.gray(` Last event: ${formatRelativeTime(state.last_event_ts)}`));
|
|
281
|
+
console.log('');
|
|
282
|
+
}
|
|
283
|
+
// Footer
|
|
284
|
+
console.log(chalk_1.default.cyan.bold('═══════════════════════════════════════════════════════════════'));
|
|
285
|
+
console.log('');
|
|
286
|
+
// Watch mode
|
|
287
|
+
if (options.watch) {
|
|
288
|
+
console.log(chalk_1.default.gray('Watching for changes... (Ctrl+C to exit)'));
|
|
289
|
+
const interval = setInterval(async () => {
|
|
290
|
+
console.clear();
|
|
291
|
+
await streamStatus({ ...options, watch: false });
|
|
292
|
+
console.log(chalk_1.default.gray('Watching for changes... (Ctrl+C to exit)'));
|
|
293
|
+
}, 1000);
|
|
294
|
+
process.on('SIGINT', () => {
|
|
295
|
+
clearInterval(interval);
|
|
296
|
+
process.exit(0);
|
|
297
|
+
});
|
|
298
|
+
// Keep process alive
|
|
299
|
+
await new Promise(() => { });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* List all sessions with stream data
|
|
304
|
+
*/
|
|
305
|
+
function streamList() {
|
|
306
|
+
console.log('');
|
|
307
|
+
console.log(chalk_1.default.cyan.bold('Stream Sessions'));
|
|
308
|
+
console.log('');
|
|
309
|
+
if (!fs.existsSync(STREAM_CACHE_DIR)) {
|
|
310
|
+
console.log(chalk_1.default.gray('No stream cache directory found.'));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const files = fs.readdirSync(STREAM_CACHE_DIR)
|
|
314
|
+
.filter(f => f.endsWith('.stream.jsonl'))
|
|
315
|
+
.map(f => {
|
|
316
|
+
const sessionId = f.replace('.stream.jsonl', '');
|
|
317
|
+
const stat = fs.statSync(path.join(STREAM_CACHE_DIR, f));
|
|
318
|
+
const state = loadStateMachine(sessionId);
|
|
319
|
+
return {
|
|
320
|
+
sessionId,
|
|
321
|
+
sessionName: state?.session_name,
|
|
322
|
+
state: state?.state || 'unknown',
|
|
323
|
+
mtime: stat.mtimeMs,
|
|
324
|
+
size: stat.size,
|
|
325
|
+
};
|
|
326
|
+
})
|
|
327
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
328
|
+
if (files.length === 0) {
|
|
329
|
+
console.log(chalk_1.default.gray('No stream sessions found.'));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
for (const file of files) {
|
|
333
|
+
const name = file.sessionName || file.sessionId.slice(0, 8);
|
|
334
|
+
const stateStr = formatState(file.state);
|
|
335
|
+
const sizeStr = formatBytes(file.size);
|
|
336
|
+
const timeStr = formatRelativeTime(new Date(file.mtime).toISOString());
|
|
337
|
+
console.log(` ${stateStr} ${chalk_1.default.white(name)} ${chalk_1.default.gray(`(${sizeStr}, ${timeStr})`)}`);
|
|
338
|
+
}
|
|
339
|
+
console.log('');
|
|
340
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,11 +10,13 @@ const test_1 = require("./commands/test");
|
|
|
10
10
|
const status_1 = require("./commands/status");
|
|
11
11
|
const run_1 = require("./commands/run");
|
|
12
12
|
const doctor_1 = require("./commands/doctor");
|
|
13
|
+
const stream_1 = require("./commands/stream");
|
|
14
|
+
const state_1 = require("./utils/state");
|
|
13
15
|
const chalk_1 = __importDefault(require("chalk"));
|
|
14
16
|
commander_1.program
|
|
15
17
|
.name('ekkos')
|
|
16
18
|
.description('ekkOS memory CLI for AI coding assistants')
|
|
17
|
-
.version('0.2.
|
|
19
|
+
.version('0.2.8')
|
|
18
20
|
.addHelpText('beforeAll', chalk_1.default.cyan('\n made by ekkOS_ with ❤️\n'));
|
|
19
21
|
// Main init command (combined auth + setup)
|
|
20
22
|
commander_1.program
|
|
@@ -61,6 +63,61 @@ commander_1.program
|
|
|
61
63
|
.action((options) => {
|
|
62
64
|
(0, doctor_1.doctor)({ fix: options.fix, json: options.json });
|
|
63
65
|
});
|
|
66
|
+
// Stream command - stream capture status and management
|
|
67
|
+
const streamCmd = commander_1.program
|
|
68
|
+
.command('stream')
|
|
69
|
+
.description('Stream capture status and management');
|
|
70
|
+
streamCmd
|
|
71
|
+
.command('status')
|
|
72
|
+
.description('Show stream capture status for current session')
|
|
73
|
+
.option('-s, --session <id>', 'Session ID to check')
|
|
74
|
+
.option('-w, --watch', 'Watch mode - refresh every second')
|
|
75
|
+
.option('-j, --json', 'Output machine-readable JSON')
|
|
76
|
+
.action((options) => {
|
|
77
|
+
(0, stream_1.streamStatus)({
|
|
78
|
+
session: options.session,
|
|
79
|
+
watch: options.watch,
|
|
80
|
+
json: options.json
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
streamCmd
|
|
84
|
+
.command('list')
|
|
85
|
+
.description('List all sessions with stream data')
|
|
86
|
+
.action(() => {
|
|
87
|
+
(0, stream_1.streamList)();
|
|
88
|
+
});
|
|
89
|
+
// Sessions command - list active Claude Code sessions (swarm support)
|
|
90
|
+
commander_1.program
|
|
91
|
+
.command('sessions')
|
|
92
|
+
.description('List active Claude Code sessions (for swarm/multi-session support)')
|
|
93
|
+
.option('-j, --json', 'Output machine-readable JSON')
|
|
94
|
+
.action((options) => {
|
|
95
|
+
const sessions = (0, state_1.getActiveSessions)();
|
|
96
|
+
if (options.json) {
|
|
97
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(chalk_1.default.cyan.bold('🐝 Active ekkOS Sessions'));
|
|
102
|
+
console.log('');
|
|
103
|
+
if (sessions.length === 0) {
|
|
104
|
+
console.log(chalk_1.default.gray(' No active sessions found.'));
|
|
105
|
+
console.log(chalk_1.default.gray(' Run `ekkos run` to start a session.'));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.log(chalk_1.default.gray(` ${sessions.length} active session${sessions.length > 1 ? 's' : ''}:`));
|
|
109
|
+
console.log('');
|
|
110
|
+
for (const session of sessions) {
|
|
111
|
+
const age = Math.round((Date.now() - new Date(session.startedAt).getTime()) / 1000 / 60);
|
|
112
|
+
const lastBeat = Math.round((Date.now() - new Date(session.lastHeartbeat).getTime()) / 1000);
|
|
113
|
+
console.log(` ${chalk_1.default.green('●')} ${chalk_1.default.bold(session.sessionName)}`);
|
|
114
|
+
console.log(` ${chalk_1.default.gray('PID:')} ${session.pid}`);
|
|
115
|
+
console.log(` ${chalk_1.default.gray('Path:')} ${session.projectPath}`);
|
|
116
|
+
console.log(` ${chalk_1.default.gray('Age:')} ${age} min ${chalk_1.default.gray('Last seen:')} ${lastBeat}s ago`);
|
|
117
|
+
console.log('');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
64
121
|
// Deprecated setup command (redirects to init)
|
|
65
122
|
commander_1.program
|
|
66
123
|
.command('setup')
|
|
@@ -20,6 +20,12 @@ export declare class RestoreOrchestrator {
|
|
|
20
20
|
* Main restore function - attempts tiers in order
|
|
21
21
|
*/
|
|
22
22
|
restore(options?: RestoreOptions): Promise<CacheResult<RestorePayload>>;
|
|
23
|
+
/**
|
|
24
|
+
* Tier -1: Restore from stream log (has mid-turn content)
|
|
25
|
+
* This is checked FIRST because stream logs have the most recent data,
|
|
26
|
+
* including in-progress turns that haven't been sealed yet.
|
|
27
|
+
*/
|
|
28
|
+
private restoreFromStreamLog;
|
|
23
29
|
/**
|
|
24
30
|
* Tier 0: Restore from local JSONL cache
|
|
25
31
|
*/
|
|
@@ -49,9 +49,11 @@ const path = __importStar(require("path"));
|
|
|
49
49
|
const os = __importStar(require("os"));
|
|
50
50
|
const LocalSessionStore_js_1 = require("../cache/LocalSessionStore.js");
|
|
51
51
|
const types_js_1 = require("../cache/types.js");
|
|
52
|
+
const stream_tailer_js_1 = require("../capture/stream-tailer.js");
|
|
52
53
|
// API configuration
|
|
53
54
|
const MEMORY_API_URL = process.env.EKKOS_API_URL || 'https://api.ekkos.dev';
|
|
54
55
|
const CONFIG_PATH = path.join(os.homedir(), '.ekkos', 'config.json');
|
|
56
|
+
const STREAM_CACHE_DIR = path.join(os.homedir(), '.ekkos', 'cache', 'sessions');
|
|
55
57
|
/**
|
|
56
58
|
* Load auth token from config
|
|
57
59
|
*/
|
|
@@ -102,6 +104,14 @@ class RestoreOrchestrator {
|
|
|
102
104
|
latency_ms: Date.now() - startTime,
|
|
103
105
|
};
|
|
104
106
|
}
|
|
107
|
+
// Try Tier -1: Stream log (has mid-turn content, most recent data)
|
|
108
|
+
const streamResult = await this.restoreFromStreamLog(sessionId, sessionName || '', lastN);
|
|
109
|
+
if (streamResult.success && streamResult.data) {
|
|
110
|
+
return {
|
|
111
|
+
...streamResult,
|
|
112
|
+
latency_ms: Date.now() - startTime,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
105
115
|
// Try Tier 0: Local cache
|
|
106
116
|
const localResult = await this.restoreFromLocal(sessionId, sessionName || '', lastN);
|
|
107
117
|
if (localResult.success && localResult.data) {
|
|
@@ -133,6 +143,117 @@ class RestoreOrchestrator {
|
|
|
133
143
|
latency_ms: Date.now() - startTime,
|
|
134
144
|
};
|
|
135
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Tier -1: Restore from stream log (has mid-turn content)
|
|
148
|
+
* This is checked FIRST because stream logs have the most recent data,
|
|
149
|
+
* including in-progress turns that haven't been sealed yet.
|
|
150
|
+
*/
|
|
151
|
+
async restoreFromStreamLog(sessionId, sessionName, lastN) {
|
|
152
|
+
const startTime = Date.now();
|
|
153
|
+
// Find stream log file
|
|
154
|
+
const streamLogPath = path.join(STREAM_CACHE_DIR, `${sessionId}.stream.jsonl`);
|
|
155
|
+
if (!fs.existsSync(streamLogPath)) {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
error: 'No stream log found',
|
|
159
|
+
source: 'stream',
|
|
160
|
+
latency_ms: Date.now() - startTime,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const { turns: streamTurns, latestTurnId } = await (0, stream_tailer_js_1.reconstructTurnFromEvents)(streamLogPath);
|
|
165
|
+
if (streamTurns.size === 0) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: 'Stream log empty',
|
|
169
|
+
source: 'stream',
|
|
170
|
+
latency_ms: Date.now() - startTime,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// Get the latest turn (may be in_progress)
|
|
174
|
+
const latestTurn = streamTurns.get(latestTurnId);
|
|
175
|
+
if (!latestTurn) {
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
error: 'Could not find latest turn in stream',
|
|
179
|
+
source: 'stream',
|
|
180
|
+
latency_ms: Date.now() - startTime,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// Check if latest turn is in_progress (mid-generation)
|
|
184
|
+
const isInProgress = latestTurn.status === 'in_progress';
|
|
185
|
+
// Build turns array
|
|
186
|
+
const turnsArray = [];
|
|
187
|
+
for (const [turnId, turn] of streamTurns) {
|
|
188
|
+
turnsArray.push({
|
|
189
|
+
turn_id: turnId,
|
|
190
|
+
ts: new Date().toISOString(),
|
|
191
|
+
user_query: turn.user_query,
|
|
192
|
+
assistant_response: turn.assistant_stream, // Use stream content, not sealed response
|
|
193
|
+
tools_used: turn.tools.filter(t => t.kind === 'tool_use').map(t => t.name || 'unknown'),
|
|
194
|
+
files_referenced: [],
|
|
195
|
+
is_complete: turn.status === 'complete',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// Sort by turn_id
|
|
199
|
+
turnsArray.sort((a, b) => a.turn_id - b.turn_id);
|
|
200
|
+
// Get last N turns
|
|
201
|
+
const restoredTurns = turnsArray.slice(-lastN);
|
|
202
|
+
// For in-progress turns, apply middle-truncation (preserve tail)
|
|
203
|
+
let latestAssistantStream = latestTurn.assistant_stream;
|
|
204
|
+
if (isInProgress && latestAssistantStream.length > 12000) {
|
|
205
|
+
latestAssistantStream = (0, stream_tailer_js_1.middleTruncate)(latestAssistantStream, {
|
|
206
|
+
headChars: 2000,
|
|
207
|
+
tailChars: 8000,
|
|
208
|
+
maxTotal: 12000,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Detect open loops (incomplete tools)
|
|
212
|
+
const openLoops = (0, stream_tailer_js_1.detectOpenLoops)(latestTurn.tools);
|
|
213
|
+
const payload = {
|
|
214
|
+
session_id: sessionId,
|
|
215
|
+
session_name: sessionName || 'unknown',
|
|
216
|
+
source: 'stream',
|
|
217
|
+
restored_turns: restoredTurns,
|
|
218
|
+
latest: {
|
|
219
|
+
user_query: latestTurn.user_query,
|
|
220
|
+
assistant_response: latestAssistantStream,
|
|
221
|
+
},
|
|
222
|
+
pending_turn: isInProgress
|
|
223
|
+
? {
|
|
224
|
+
turn_id: latestTurnId,
|
|
225
|
+
user_query: latestTurn.user_query,
|
|
226
|
+
ts: new Date().toISOString(),
|
|
227
|
+
}
|
|
228
|
+
: undefined,
|
|
229
|
+
directives: [],
|
|
230
|
+
patterns: [],
|
|
231
|
+
metadata: {
|
|
232
|
+
acked_turn_id: latestTurnId,
|
|
233
|
+
last_flush_ts: new Date().toISOString(),
|
|
234
|
+
token_estimate: Math.ceil(latestAssistantStream.length / 4),
|
|
235
|
+
complete_turn_count: turnsArray.filter(t => t.is_complete).length,
|
|
236
|
+
has_pending: isInProgress,
|
|
237
|
+
is_stream_restore: true,
|
|
238
|
+
open_loops: openLoops,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
data: payload,
|
|
244
|
+
source: 'stream',
|
|
245
|
+
latency_ms: Date.now() - startTime,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
error: err instanceof Error ? err.message : String(err),
|
|
252
|
+
source: 'stream',
|
|
253
|
+
latency_ms: Date.now() - startTime,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
136
257
|
/**
|
|
137
258
|
* Tier 0: Restore from local JSONL cache
|
|
138
259
|
*/
|
|
@@ -432,35 +553,66 @@ class RestoreOrchestrator {
|
|
|
432
553
|
* Format RestorePayload as system-reminder markdown
|
|
433
554
|
*/
|
|
434
555
|
formatAsSystemReminder(payload) {
|
|
556
|
+
const isStreamRestore = payload.metadata?.is_stream_restore;
|
|
557
|
+
const isInProgress = payload.metadata?.has_pending && isStreamRestore;
|
|
558
|
+
const openLoops = payload.metadata?.open_loops;
|
|
435
559
|
const lines = [
|
|
436
560
|
'<system-reminder>',
|
|
437
561
|
'CONTEXT RESTORED (ekkOS /continue)',
|
|
438
562
|
`Session: ${payload.session_name} (${payload.session_id})`,
|
|
439
|
-
`Source: ${payload.source}`,
|
|
440
|
-
`Turns restored: ${payload.restored_turns.length}`,
|
|
441
|
-
'',
|
|
442
|
-
'## Last User Request',
|
|
443
|
-
payload.latest.user_query,
|
|
444
|
-
'',
|
|
445
|
-
'## Last Assistant Response',
|
|
446
|
-
payload.latest.assistant_response.slice(0, 2000),
|
|
447
|
-
payload.latest.assistant_response.length > 2000 ? '\n[...truncated...]' : '',
|
|
563
|
+
`Source: ${payload.source}${isStreamRestore ? ' (real-time stream)' : ''}`,
|
|
564
|
+
isInProgress ? `Status: IN_PROGRESS (mid-turn restore)` : `Turns restored: ${payload.restored_turns.length}`,
|
|
448
565
|
'',
|
|
449
|
-
'## Recent Turns (older → newer)',
|
|
450
566
|
];
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
567
|
+
// For stream restores with in-progress turns, show special format
|
|
568
|
+
if (isInProgress && isStreamRestore) {
|
|
569
|
+
// Show open loops if any
|
|
570
|
+
if (openLoops && openLoops.length > 0) {
|
|
571
|
+
lines.push('## Open Loops (machine-derived)');
|
|
572
|
+
for (const loop of openLoops) {
|
|
573
|
+
lines.push(`- ⚠️ ${loop.detail}`);
|
|
574
|
+
}
|
|
575
|
+
lines.push('');
|
|
576
|
+
}
|
|
577
|
+
// Show last user request
|
|
578
|
+
lines.push('## Last User Request');
|
|
579
|
+
lines.push(payload.latest.user_query);
|
|
580
|
+
lines.push('');
|
|
581
|
+
// Show assistant stream with tail preserved
|
|
582
|
+
lines.push('## Assistant Stream (tail-preserved for resume)');
|
|
583
|
+
lines.push(payload.latest.assistant_response);
|
|
584
|
+
lines.push('');
|
|
585
|
+
// Special instruction for mid-turn resume
|
|
586
|
+
lines.push('INSTRUCTION: Continue from the exact point shown above.');
|
|
587
|
+
lines.push('Do not recap. Do not restart. Continue the sentence/action if mid-sentence.');
|
|
588
|
+
lines.push('Start your response with: "✓ Continuing -"');
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// Standard restore format
|
|
592
|
+
lines.push('## Last User Request');
|
|
593
|
+
lines.push(payload.latest.user_query);
|
|
594
|
+
lines.push('');
|
|
595
|
+
lines.push('## Last Assistant Response');
|
|
596
|
+
lines.push(payload.latest.assistant_response.slice(0, 2000));
|
|
597
|
+
if (payload.latest.assistant_response.length > 2000) {
|
|
598
|
+
lines.push('\n[...truncated...]');
|
|
599
|
+
}
|
|
600
|
+
lines.push('');
|
|
601
|
+
lines.push('## Recent Turns (older → newer)');
|
|
602
|
+
// Add turn summaries (skip last one since it's shown in detail above)
|
|
603
|
+
const turnsToShow = payload.restored_turns.slice(0, -1);
|
|
604
|
+
for (let i = 0; i < turnsToShow.length; i++) {
|
|
605
|
+
const turn = turnsToShow[i];
|
|
606
|
+
const userSnippet = turn.user_query.slice(0, 100) + (turn.user_query.length > 100 ? '...' : '');
|
|
607
|
+
const assistantSnippet = turn.assistant_response.slice(0, 100) + (turn.assistant_response.length > 100 ? '...' : '');
|
|
608
|
+
lines.push(`${i + 1}) U: ${userSnippet}`);
|
|
609
|
+
lines.push(` A: ${assistantSnippet}`);
|
|
610
|
+
}
|
|
611
|
+
lines.push('');
|
|
612
|
+
lines.push('INSTRUCTION: Resume seamlessly where you left off.');
|
|
613
|
+
lines.push('Do not ask "what were we doing?"');
|
|
614
|
+
lines.push('Start your response with: "✓ Continuing -"');
|
|
459
615
|
}
|
|
460
|
-
lines.push('');
|
|
461
|
-
lines.push('INSTRUCTION: Resume seamlessly where you left off.');
|
|
462
|
-
lines.push('Do not ask "what were we doing?"');
|
|
463
|
-
lines.push('Start your response with: "✓ Continuing -"');
|
|
464
616
|
lines.push('</system-reminder>');
|
|
465
617
|
return lines.join('\n');
|
|
466
618
|
}
|