@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.
@@ -1,7 +1,16 @@
1
1
  export declare const EKKOS_DIR: string;
2
2
  export declare const STATE_FILE: string;
3
+ export declare const ACTIVE_SESSIONS_FILE: string;
3
4
  export declare const AUTO_CLEAR_FLAG: string;
4
5
  export declare const CONFIG_FILE: string;
6
+ export interface ActiveSession {
7
+ sessionId: string;
8
+ sessionName: string;
9
+ pid: number;
10
+ startedAt: string;
11
+ projectPath: string;
12
+ lastHeartbeat: string;
13
+ }
5
14
  export interface EkkosState {
6
15
  sessionName: string;
7
16
  sessionId: string;
@@ -20,7 +29,7 @@ export interface EkkosConfig {
20
29
  */
21
30
  export declare function uuidToWords(uuid: string): string;
22
31
  /**
23
- * Ensure .ekkos directory exists
32
+ * Ensure .ekkos directory exists and has required files
24
33
  */
25
34
  export declare function ensureEkkosDir(): void;
26
35
  /**
@@ -64,3 +73,32 @@ export declare function getMostRecentSession(): {
64
73
  sessionId: string;
65
74
  lastActive: string;
66
75
  } | null;
76
+ /**
77
+ * Get all active sessions (filters out stale ones with dead PIDs)
78
+ */
79
+ export declare function getActiveSessions(): ActiveSession[];
80
+ /**
81
+ * Register a new active session (for swarm tracking)
82
+ */
83
+ export declare function registerActiveSession(sessionId: string, sessionName: string, projectPath: string): ActiveSession;
84
+ /**
85
+ * Update heartbeat for current process's session
86
+ */
87
+ export declare function heartbeatActiveSession(): void;
88
+ /**
89
+ * Unregister current process's session (on clean exit)
90
+ */
91
+ export declare function unregisterActiveSession(): void;
92
+ /**
93
+ * Get the active session for current PID (if any)
94
+ */
95
+ export declare function getCurrentProcessSession(): ActiveSession | null;
96
+ /**
97
+ * Get active session by session name
98
+ */
99
+ export declare function getActiveSessionByName(sessionName: string): ActiveSession | null;
100
+ /**
101
+ * Update the session for the current PID (when session name is detected)
102
+ * This does NOT write to the global state.json - only to active-sessions.json
103
+ */
104
+ export declare function updateCurrentProcessSession(sessionId: string, sessionName: string): void;
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.CONFIG_FILE = exports.AUTO_CLEAR_FLAG = exports.STATE_FILE = exports.EKKOS_DIR = void 0;
39
+ exports.CONFIG_FILE = exports.AUTO_CLEAR_FLAG = exports.ACTIVE_SESSIONS_FILE = exports.STATE_FILE = exports.EKKOS_DIR = void 0;
40
40
  exports.uuidToWords = uuidToWords;
41
41
  exports.ensureEkkosDir = ensureEkkosDir;
42
42
  exports.getState = getState;
@@ -47,12 +47,20 @@ exports.getAuthToken = getAuthToken;
47
47
  exports.clearAutoClearFlag = clearAutoClearFlag;
48
48
  exports.parseAutoClearFlag = parseAutoClearFlag;
49
49
  exports.getMostRecentSession = getMostRecentSession;
50
+ exports.getActiveSessions = getActiveSessions;
51
+ exports.registerActiveSession = registerActiveSession;
52
+ exports.heartbeatActiveSession = heartbeatActiveSession;
53
+ exports.unregisterActiveSession = unregisterActiveSession;
54
+ exports.getCurrentProcessSession = getCurrentProcessSession;
55
+ exports.getActiveSessionByName = getActiveSessionByName;
56
+ exports.updateCurrentProcessSession = updateCurrentProcessSession;
50
57
  const fs = __importStar(require("fs"));
51
58
  const path = __importStar(require("path"));
52
59
  const os = __importStar(require("os"));
53
60
  // State file paths
54
61
  exports.EKKOS_DIR = path.join(os.homedir(), '.ekkos');
55
62
  exports.STATE_FILE = path.join(exports.EKKOS_DIR, 'state.json');
63
+ exports.ACTIVE_SESSIONS_FILE = path.join(exports.EKKOS_DIR, 'active-sessions.json');
56
64
  exports.AUTO_CLEAR_FLAG = path.join(exports.EKKOS_DIR, 'auto-clear.flag');
57
65
  exports.CONFIG_FILE = path.join(exports.EKKOS_DIR, 'config.json');
58
66
  // SINGLE SOURCE OF TRUTH: Import word lists from shared JSON
@@ -81,12 +89,27 @@ function uuidToWords(uuid) {
81
89
  return `${ADJECTIVES[adjIdx]}-${NOUNS[nounIdx]}-${VERBS[verbIdx]}`;
82
90
  }
83
91
  /**
84
- * Ensure .ekkos directory exists
92
+ * Ensure .ekkos directory exists and has required files
85
93
  */
86
94
  function ensureEkkosDir() {
87
95
  if (!fs.existsSync(exports.EKKOS_DIR)) {
88
96
  fs.mkdirSync(exports.EKKOS_DIR, { recursive: true });
89
97
  }
98
+ // Copy session-words.json to ~/.ekkos/ if not present
99
+ // This is the SINGLE SOURCE OF TRUTH for session name generation
100
+ // Used by: CLI, hooks, Time Machine, extension
101
+ const destWords = path.join(exports.EKKOS_DIR, 'session-words.json');
102
+ if (!fs.existsSync(destWords)) {
103
+ try {
104
+ const srcWords = path.join(__dirname, 'session-words.json');
105
+ if (fs.existsSync(srcWords)) {
106
+ fs.copyFileSync(srcWords, destWords);
107
+ }
108
+ }
109
+ catch {
110
+ // Ignore - file will be created manually or by another component
111
+ }
112
+ }
90
113
  }
91
114
  /**
92
115
  * Get current session state
@@ -220,3 +243,130 @@ function getMostRecentSession() {
220
243
  return null;
221
244
  }
222
245
  }
246
+ // ═══════════════════════════════════════════════════════════════════════════
247
+ // MULTI-SESSION/SWARM SUPPORT
248
+ // Track multiple concurrent Claude Code sessions without state collision
249
+ // ═══════════════════════════════════════════════════════════════════════════
250
+ /**
251
+ * Get all active sessions (filters out stale ones with dead PIDs)
252
+ */
253
+ function getActiveSessions() {
254
+ ensureEkkosDir();
255
+ try {
256
+ if (!fs.existsSync(exports.ACTIVE_SESSIONS_FILE)) {
257
+ return [];
258
+ }
259
+ const content = fs.readFileSync(exports.ACTIVE_SESSIONS_FILE, 'utf-8');
260
+ const sessions = JSON.parse(content);
261
+ // Filter out sessions with dead PIDs
262
+ const alive = sessions.filter(s => isProcessAlive(s.pid));
263
+ // If we filtered any out, update the file
264
+ if (alive.length !== sessions.length) {
265
+ fs.writeFileSync(exports.ACTIVE_SESSIONS_FILE, JSON.stringify(alive, null, 2));
266
+ }
267
+ return alive;
268
+ }
269
+ catch {
270
+ return [];
271
+ }
272
+ }
273
+ /**
274
+ * Register a new active session (for swarm tracking)
275
+ */
276
+ function registerActiveSession(sessionId, sessionName, projectPath) {
277
+ ensureEkkosDir();
278
+ const sessions = getActiveSessions();
279
+ const now = new Date().toISOString();
280
+ const pid = process.pid;
281
+ // Remove any existing entry for this PID (process restart)
282
+ const filtered = sessions.filter(s => s.pid !== pid);
283
+ const newSession = {
284
+ sessionId,
285
+ sessionName,
286
+ pid,
287
+ startedAt: now,
288
+ projectPath,
289
+ lastHeartbeat: now
290
+ };
291
+ filtered.push(newSession);
292
+ fs.writeFileSync(exports.ACTIVE_SESSIONS_FILE, JSON.stringify(filtered, null, 2));
293
+ return newSession;
294
+ }
295
+ /**
296
+ * Update heartbeat for current process's session
297
+ */
298
+ function heartbeatActiveSession() {
299
+ try {
300
+ const sessions = getActiveSessions();
301
+ const pid = process.pid;
302
+ const idx = sessions.findIndex(s => s.pid === pid);
303
+ if (idx !== -1) {
304
+ sessions[idx].lastHeartbeat = new Date().toISOString();
305
+ fs.writeFileSync(exports.ACTIVE_SESSIONS_FILE, JSON.stringify(sessions, null, 2));
306
+ }
307
+ }
308
+ catch {
309
+ // Ignore heartbeat errors
310
+ }
311
+ }
312
+ /**
313
+ * Unregister current process's session (on clean exit)
314
+ */
315
+ function unregisterActiveSession() {
316
+ try {
317
+ const sessions = getActiveSessions();
318
+ const pid = process.pid;
319
+ const filtered = sessions.filter(s => s.pid !== pid);
320
+ fs.writeFileSync(exports.ACTIVE_SESSIONS_FILE, JSON.stringify(filtered, null, 2));
321
+ }
322
+ catch {
323
+ // Ignore unregister errors on exit
324
+ }
325
+ }
326
+ /**
327
+ * Get the active session for current PID (if any)
328
+ */
329
+ function getCurrentProcessSession() {
330
+ const sessions = getActiveSessions();
331
+ return sessions.find(s => s.pid === process.pid) || null;
332
+ }
333
+ /**
334
+ * Get active session by session name
335
+ */
336
+ function getActiveSessionByName(sessionName) {
337
+ const sessions = getActiveSessions();
338
+ return sessions.find(s => s.sessionName === sessionName) || null;
339
+ }
340
+ /**
341
+ * Check if a process is still alive
342
+ */
343
+ function isProcessAlive(pid) {
344
+ try {
345
+ // Sending signal 0 doesn't kill the process, just checks if it exists
346
+ process.kill(pid, 0);
347
+ return true;
348
+ }
349
+ catch {
350
+ return false;
351
+ }
352
+ }
353
+ /**
354
+ * Update the session for the current PID (when session name is detected)
355
+ * This does NOT write to the global state.json - only to active-sessions.json
356
+ */
357
+ function updateCurrentProcessSession(sessionId, sessionName) {
358
+ try {
359
+ const sessions = getActiveSessions();
360
+ const pid = process.pid;
361
+ const idx = sessions.findIndex(s => s.pid === pid);
362
+ if (idx !== -1) {
363
+ sessions[idx].sessionId = sessionId;
364
+ sessions[idx].sessionName = sessionName;
365
+ sessions[idx].lastHeartbeat = new Date().toISOString();
366
+ fs.writeFileSync(exports.ACTIVE_SESSIONS_FILE, JSON.stringify(sessions, null, 2));
367
+ }
368
+ }
369
+ catch {
370
+ // Ignore errors
371
+ }
372
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {