@ekkos/cli 0.2.8 → 0.2.10

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.
Files changed (36) hide show
  1. package/dist/cache/LocalSessionStore.d.ts +34 -21
  2. package/dist/cache/LocalSessionStore.js +169 -53
  3. package/dist/cache/capture.d.ts +19 -11
  4. package/dist/cache/capture.js +243 -76
  5. package/dist/cache/types.d.ts +14 -1
  6. package/dist/commands/doctor.d.ts +10 -0
  7. package/dist/commands/doctor.js +148 -73
  8. package/dist/commands/hooks.d.ts +109 -0
  9. package/dist/commands/hooks.js +668 -0
  10. package/dist/commands/run.d.ts +1 -0
  11. package/dist/commands/run.js +69 -21
  12. package/dist/index.js +42 -1
  13. package/dist/restore/RestoreOrchestrator.d.ts +17 -3
  14. package/dist/restore/RestoreOrchestrator.js +64 -22
  15. package/dist/utils/paths.d.ts +125 -0
  16. package/dist/utils/paths.js +283 -0
  17. package/dist/utils/session-words.json +30 -111
  18. package/package.json +1 -1
  19. package/templates/ekkos-manifest.json +223 -0
  20. package/templates/helpers/json-parse.cjs +101 -0
  21. package/templates/hooks/assistant-response.ps1 +256 -0
  22. package/templates/hooks/assistant-response.sh +124 -64
  23. package/templates/hooks/session-start.ps1 +107 -2
  24. package/templates/hooks/session-start.sh +201 -166
  25. package/templates/hooks/stop.ps1 +124 -3
  26. package/templates/hooks/stop.sh +470 -843
  27. package/templates/hooks/user-prompt-submit.ps1 +107 -22
  28. package/templates/hooks/user-prompt-submit.sh +403 -393
  29. package/templates/project-stubs/session-start.ps1 +63 -0
  30. package/templates/project-stubs/session-start.sh +55 -0
  31. package/templates/project-stubs/stop.ps1 +63 -0
  32. package/templates/project-stubs/stop.sh +55 -0
  33. package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
  34. package/templates/project-stubs/user-prompt-submit.sh +55 -0
  35. package/templates/shared/hooks-enabled.json +22 -0
  36. package/templates/shared/session-words.json +45 -0
@@ -1,39 +1,41 @@
1
1
  /**
2
- * ekkOS Fast /continue - Local Session Store
2
+ * ekkOS Fast /continue - Local Session Store (Instance-Aware)
3
3
  *
4
4
  * Tier 0 of the 3-tier restore chain.
5
5
  * Provides ultra-low latency (<20ms) turn storage using append-only JSONL files.
6
6
  *
7
+ * Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
8
+ * - All Tier 0 cache paths MUST be: ~/.ekkos/cache/sessions/{instanceId}/{sessionId}.jsonl
9
+ * - All persisted records MUST include: instanceId, sessionId, sessionName
10
+ * - Backward compatibility: missing instanceId treated as 'default'
11
+ *
7
12
  * Features:
13
+ * - Instance-scoped storage (prevents multi-session cross-talk)
8
14
  * - Atomic writes (write to .tmp, rename)
9
15
  * - Crash recovery (handle partial JSONL lines)
10
16
  * - ACK-based safe pruning
11
17
  * - Multi-session support with LRU eviction
18
+ * - Legacy path fallback for reads (backward compatibility)
12
19
  */
13
20
  import { Turn, SessionMeta, LocalCacheConfig, SessionListEntry, CacheResult } from './types.js';
14
21
  /**
15
- * LocalSessionStore - Fast local cache for conversation turns
22
+ * LocalSessionStore - Fast local cache for conversation turns (Instance-Aware)
16
23
  */
17
24
  export declare class LocalSessionStore {
18
25
  private config;
19
- private sessionsDir;
20
- private indexPath;
26
+ private instanceId;
21
27
  private indexCache;
22
28
  private indexCacheTime;
23
29
  private readonly INDEX_CACHE_TTL;
24
- constructor(config?: Partial<LocalCacheConfig>);
25
- /**
26
- * Ensure cache directories exist
27
- */
28
- private ensureDirectories;
30
+ constructor(config?: Partial<LocalCacheConfig>, instanceId?: string);
29
31
  /**
30
- * Get the JSONL file path for a session
32
+ * Get the current instance ID
31
33
  */
32
- private getTurnsPath;
34
+ getInstanceId(): string;
33
35
  /**
34
- * Get the metadata file path for a session
36
+ * Set the instance ID (useful for switching contexts)
35
37
  */
36
- private getMetaPath;
38
+ setInstanceId(instanceId: string): void;
37
39
  /**
38
40
  * Read the session index (with caching)
39
41
  */
@@ -47,21 +49,24 @@ export declare class LocalSessionStore {
47
49
  */
48
50
  private updateIndexEntry;
49
51
  /**
50
- * Read session metadata
52
+ * Read session metadata (checks instance path, then legacy path)
51
53
  */
52
54
  getSessionMeta(sessionId: string): SessionMeta | null;
53
55
  /**
54
- * Write session metadata atomically
56
+ * Write session metadata atomically (always to instance-scoped path)
55
57
  */
56
58
  private writeSessionMeta;
57
59
  /**
58
60
  * Append a turn to the session's JSONL file
59
61
  * This is the hot path - must be fast
62
+ *
63
+ * Per v1.2 ADDENDUM: All records include instanceId, sessionId, sessionName
60
64
  */
61
65
  appendTurn(sessionId: string, sessionName: string, turn: Turn, projectPath?: string): CacheResult<void>;
62
66
  /**
63
67
  * Get the last N turns from a session
64
68
  * Handles crash recovery (partial lines)
69
+ * Checks instance path first, then legacy path as read-only fallback
65
70
  */
66
71
  getLastTurns(sessionId: string, n?: number): CacheResult<Turn[]>;
67
72
  /**
@@ -89,19 +94,25 @@ export declare class LocalSessionStore {
89
94
  */
90
95
  prune(sessionId: string): CacheResult<number>;
91
96
  /**
92
- * List all sessions, sorted by last active (newest first)
97
+ * List all sessions for current instance, sorted by last active (newest first)
93
98
  */
94
99
  listSessions(): SessionListEntry[];
95
100
  /**
96
- * Get session ID from session name
101
+ * List all sessions across ALL instances (for global queries)
102
+ */
103
+ listAllSessions(): Array<SessionListEntry & {
104
+ instance_id: string;
105
+ }>;
106
+ /**
107
+ * Get session ID from session name (searches current instance)
97
108
  */
98
109
  getSessionId(sessionName: string): string | null;
99
110
  /**
100
- * Get session name from session ID
111
+ * Get session name from session ID (searches current instance)
101
112
  */
102
113
  getSessionName(sessionId: string): string | null;
103
114
  /**
104
- * Check if a session exists in local cache
115
+ * Check if a session exists in local cache (current instance or legacy)
105
116
  */
106
117
  hasSession(sessionIdOrName: string): boolean;
107
118
  /**
@@ -114,16 +125,18 @@ export declare class LocalSessionStore {
114
125
  */
115
126
  evictOldSessions(): number;
116
127
  /**
117
- * Get cache statistics
128
+ * Get cache statistics for current instance
118
129
  */
119
130
  getStats(): {
120
131
  session_count: number;
121
132
  total_turns: number;
122
133
  cache_size_bytes: number;
134
+ instance_id: string;
123
135
  };
124
136
  /**
125
- * Clear all local cache (use with caution)
137
+ * Clear all local cache for current instance (use with caution)
126
138
  */
127
139
  clearAll(): void;
128
140
  }
141
+ export declare function createLocalSessionStore(instanceId?: string): LocalSessionStore;
129
142
  export declare const localCache: LocalSessionStore;
@@ -1,15 +1,22 @@
1
1
  "use strict";
2
2
  /**
3
- * ekkOS Fast /continue - Local Session Store
3
+ * ekkOS Fast /continue - Local Session Store (Instance-Aware)
4
4
  *
5
5
  * Tier 0 of the 3-tier restore chain.
6
6
  * Provides ultra-low latency (<20ms) turn storage using append-only JSONL files.
7
7
  *
8
+ * Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
9
+ * - All Tier 0 cache paths MUST be: ~/.ekkos/cache/sessions/{instanceId}/{sessionId}.jsonl
10
+ * - All persisted records MUST include: instanceId, sessionId, sessionName
11
+ * - Backward compatibility: missing instanceId treated as 'default'
12
+ *
8
13
  * Features:
14
+ * - Instance-scoped storage (prevents multi-session cross-talk)
9
15
  * - Atomic writes (write to .tmp, rename)
10
16
  * - Crash recovery (handle partial JSONL lines)
11
17
  * - ACK-based safe pruning
12
18
  * - Multi-session support with LRU eviction
19
+ * - Legacy path fallback for reads (backward compatibility)
13
20
  */
14
21
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
22
  if (k2 === undefined) k2 = k;
@@ -46,9 +53,11 @@ var __importStar = (this && this.__importStar) || (function () {
46
53
  })();
47
54
  Object.defineProperty(exports, "__esModule", { value: true });
48
55
  exports.localCache = exports.LocalSessionStore = void 0;
56
+ exports.createLocalSessionStore = createLocalSessionStore;
49
57
  const fs = __importStar(require("fs"));
50
58
  const path = __importStar(require("path"));
51
59
  const os = __importStar(require("os"));
60
+ const paths_js_1 = require("../utils/paths.js");
52
61
  // Default configuration
53
62
  const DEFAULT_CONFIG = {
54
63
  cache_dir: path.join(os.homedir(), '.ekkos', 'cache'),
@@ -58,40 +67,31 @@ const DEFAULT_CONFIG = {
58
67
  flush_interval_ms: 5000,
59
68
  };
60
69
  /**
61
- * LocalSessionStore - Fast local cache for conversation turns
70
+ * LocalSessionStore - Fast local cache for conversation turns (Instance-Aware)
62
71
  */
63
72
  class LocalSessionStore {
64
- constructor(config = {}) {
73
+ constructor(config = {}, instanceId) {
65
74
  this.indexCache = null;
66
75
  this.indexCacheTime = 0;
67
76
  this.INDEX_CACHE_TTL = 1000; // 1 second
68
77
  this.config = { ...DEFAULT_CONFIG, ...config };
69
- this.sessionsDir = path.join(this.config.cache_dir, 'sessions');
70
- this.indexPath = path.join(this.sessionsDir, 'index.json');
71
- this.ensureDirectories();
78
+ this.instanceId = (0, paths_js_1.normalizeInstanceId)(instanceId || process.env.EKKOS_INSTANCE_ID);
79
+ (0, paths_js_1.ensureBaseDirs)();
80
+ (0, paths_js_1.ensureInstanceDir)(this.instanceId);
72
81
  }
73
82
  /**
74
- * Ensure cache directories exist
83
+ * Get the current instance ID
75
84
  */
76
- ensureDirectories() {
77
- try {
78
- fs.mkdirSync(this.sessionsDir, { recursive: true });
79
- }
80
- catch (err) {
81
- // Directory might already exist
82
- }
85
+ getInstanceId() {
86
+ return this.instanceId;
83
87
  }
84
88
  /**
85
- * Get the JSONL file path for a session
89
+ * Set the instance ID (useful for switching contexts)
86
90
  */
87
- getTurnsPath(sessionId) {
88
- return path.join(this.sessionsDir, `${sessionId}.jsonl`);
89
- }
90
- /**
91
- * Get the metadata file path for a session
92
- */
93
- getMetaPath(sessionId) {
94
- return path.join(this.sessionsDir, `${sessionId}.meta.json`);
91
+ setInstanceId(instanceId) {
92
+ this.instanceId = (0, paths_js_1.normalizeInstanceId)(instanceId);
93
+ this.indexCache = null; // Clear cache when switching instances
94
+ (0, paths_js_1.ensureInstanceDir)(this.instanceId);
95
95
  }
96
96
  // ═══════════════════════════════════════════════════════════════════════════
97
97
  // INDEX OPERATIONS
@@ -104,9 +104,10 @@ class LocalSessionStore {
104
104
  if (this.indexCache && now - this.indexCacheTime < this.INDEX_CACHE_TTL) {
105
105
  return this.indexCache;
106
106
  }
107
+ const indexPath = (0, paths_js_1.getIndexPath)(this.instanceId);
107
108
  try {
108
- if (fs.existsSync(this.indexPath)) {
109
- const content = fs.readFileSync(this.indexPath, 'utf-8');
109
+ if (fs.existsSync(indexPath)) {
110
+ const content = fs.readFileSync(indexPath, 'utf-8');
110
111
  this.indexCache = JSON.parse(content);
111
112
  this.indexCacheTime = now;
112
113
  return this.indexCache;
@@ -124,10 +125,11 @@ class LocalSessionStore {
124
125
  * Write the session index atomically
125
126
  */
126
127
  writeIndex(index) {
127
- const tmpPath = `${this.indexPath}.tmp`;
128
+ const indexPath = (0, paths_js_1.getIndexPath)(this.instanceId);
129
+ const tmpPath = `${indexPath}.tmp`;
128
130
  try {
129
131
  fs.writeFileSync(tmpPath, JSON.stringify(index, null, 2), 'utf-8');
130
- fs.renameSync(tmpPath, this.indexPath);
132
+ fs.renameSync(tmpPath, indexPath);
131
133
  this.indexCache = index;
132
134
  this.indexCacheTime = Date.now();
133
135
  }
@@ -154,26 +156,52 @@ class LocalSessionStore {
154
156
  // SESSION METADATA
155
157
  // ═══════════════════════════════════════════════════════════════════════════
156
158
  /**
157
- * Read session metadata
159
+ * Read session metadata (checks instance path, then legacy path)
158
160
  */
159
161
  getSessionMeta(sessionId) {
160
- const metaPath = this.getMetaPath(sessionId);
162
+ // Try instance-scoped path first
163
+ const metaPath = (0, paths_js_1.getMetaPath)(this.instanceId, sessionId);
161
164
  try {
162
165
  if (fs.existsSync(metaPath)) {
163
166
  const content = fs.readFileSync(metaPath, 'utf-8');
164
- return JSON.parse(content);
167
+ const meta = JSON.parse(content);
168
+ // Ensure instance_id is set
169
+ if (!meta.instance_id) {
170
+ meta.instance_id = this.instanceId;
171
+ }
172
+ return meta;
165
173
  }
166
174
  }
167
175
  catch (err) {
168
176
  console.error('[LocalSessionStore] Meta read error:', err);
169
177
  }
178
+ // Try legacy path (read-only fallback)
179
+ const legacyPath = (0, paths_js_1.getLegacyMetaPath)(sessionId);
180
+ try {
181
+ if (fs.existsSync(legacyPath)) {
182
+ const content = fs.readFileSync(legacyPath, 'utf-8');
183
+ const meta = JSON.parse(content);
184
+ // Mark as default instance for legacy data
185
+ if (!meta.instance_id) {
186
+ meta.instance_id = paths_js_1.DEFAULT_INSTANCE_ID;
187
+ }
188
+ return meta;
189
+ }
190
+ }
191
+ catch {
192
+ // Ignore legacy read errors
193
+ }
170
194
  return null;
171
195
  }
172
196
  /**
173
- * Write session metadata atomically
197
+ * Write session metadata atomically (always to instance-scoped path)
174
198
  */
175
199
  writeSessionMeta(meta) {
176
- const metaPath = this.getMetaPath(meta.session_id);
200
+ // Ensure instance_id is set
201
+ if (!meta.instance_id) {
202
+ meta.instance_id = this.instanceId;
203
+ }
204
+ const metaPath = (0, paths_js_1.getMetaPath)(this.instanceId, meta.session_id);
177
205
  const tmpPath = `${metaPath}.tmp`;
178
206
  try {
179
207
  fs.writeFileSync(tmpPath, JSON.stringify(meta, null, 2), 'utf-8');
@@ -195,13 +223,24 @@ class LocalSessionStore {
195
223
  /**
196
224
  * Append a turn to the session's JSONL file
197
225
  * This is the hot path - must be fast
226
+ *
227
+ * Per v1.2 ADDENDUM: All records include instanceId, sessionId, sessionName
198
228
  */
199
229
  appendTurn(sessionId, sessionName, turn, projectPath) {
200
230
  const startTime = Date.now();
201
- const turnsPath = this.getTurnsPath(sessionId);
231
+ // Ensure instance directory exists
232
+ (0, paths_js_1.ensureInstanceDir)(this.instanceId);
233
+ const turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
202
234
  try {
235
+ // Add instance namespacing fields to turn
236
+ const enrichedTurn = {
237
+ ...turn,
238
+ instance_id: this.instanceId,
239
+ session_id: sessionId,
240
+ session_name: sessionName,
241
+ };
203
242
  // Append turn as JSONL line (atomic append on most filesystems)
204
- const line = JSON.stringify(turn) + '\n';
243
+ const line = JSON.stringify(enrichedTurn) + '\n';
205
244
  fs.appendFileSync(turnsPath, line, 'utf-8');
206
245
  // Update metadata
207
246
  let meta = this.getSessionMeta(sessionId);
@@ -209,6 +248,7 @@ class LocalSessionStore {
209
248
  meta = {
210
249
  session_id: sessionId,
211
250
  session_name: sessionName,
251
+ instance_id: this.instanceId,
212
252
  acked_turn_id: 0,
213
253
  last_flush_ts: new Date().toISOString(),
214
254
  turn_count: 0,
@@ -218,12 +258,14 @@ class LocalSessionStore {
218
258
  }
219
259
  meta.turn_count = turn.turn_id;
220
260
  meta.last_flush_ts = new Date().toISOString();
261
+ meta.instance_id = this.instanceId;
221
262
  if (projectPath)
222
263
  meta.project_path = projectPath;
223
264
  this.writeSessionMeta(meta);
224
265
  // Update index
225
266
  this.updateIndexEntry(sessionName, {
226
267
  session_id: sessionId,
268
+ instance_id: this.instanceId,
227
269
  last_active_ts: new Date().toISOString(),
228
270
  last_turn_id: turn.turn_id,
229
271
  acked_turn_id: meta.acked_turn_id,
@@ -247,11 +289,17 @@ class LocalSessionStore {
247
289
  /**
248
290
  * Get the last N turns from a session
249
291
  * Handles crash recovery (partial lines)
292
+ * Checks instance path first, then legacy path as read-only fallback
250
293
  */
251
294
  getLastTurns(sessionId, n = 10) {
252
295
  const startTime = Date.now();
253
- const turnsPath = this.getTurnsPath(sessionId);
254
- try {
296
+ // Try instance-scoped path first
297
+ let turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
298
+ let isLegacy = false;
299
+ if (!fs.existsSync(turnsPath)) {
300
+ // Try legacy path as fallback
301
+ turnsPath = (0, paths_js_1.getLegacyTurnsPath)(sessionId);
302
+ isLegacy = true;
255
303
  if (!fs.existsSync(turnsPath)) {
256
304
  return {
257
305
  success: false,
@@ -260,6 +308,8 @@ class LocalSessionStore {
260
308
  latency_ms: Date.now() - startTime,
261
309
  };
262
310
  }
311
+ }
312
+ try {
263
313
  const content = fs.readFileSync(turnsPath, 'utf-8');
264
314
  const lines = content.trim().split('\n').filter(Boolean);
265
315
  const turns = [];
@@ -267,6 +317,13 @@ class LocalSessionStore {
267
317
  for (const line of lines) {
268
318
  try {
269
319
  const turn = JSON.parse(line);
320
+ // Ensure instance fields are present (for legacy data)
321
+ if (!turn.instance_id) {
322
+ turn.instance_id = isLegacy ? paths_js_1.DEFAULT_INSTANCE_ID : this.instanceId;
323
+ }
324
+ if (!turn.session_id) {
325
+ turn.session_id = sessionId;
326
+ }
270
327
  turns.push(turn);
271
328
  }
272
329
  catch {
@@ -312,8 +369,11 @@ class LocalSessionStore {
312
369
  */
313
370
  updateTurnResponse(sessionId, turnId, response, toolsUsed, filesReferenced) {
314
371
  const startTime = Date.now();
315
- const turnsPath = this.getTurnsPath(sessionId);
316
- try {
372
+ // Try instance-scoped path first
373
+ let turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
374
+ if (!fs.existsSync(turnsPath)) {
375
+ // Try legacy path
376
+ turnsPath = (0, paths_js_1.getLegacyTurnsPath)(sessionId);
317
377
  if (!fs.existsSync(turnsPath)) {
318
378
  return {
319
379
  success: false,
@@ -322,6 +382,8 @@ class LocalSessionStore {
322
382
  latency_ms: Date.now() - startTime,
323
383
  };
324
384
  }
385
+ }
386
+ try {
325
387
  const content = fs.readFileSync(turnsPath, 'utf-8');
326
388
  const lines = content.trim().split('\n').filter(Boolean);
327
389
  const turns = [];
@@ -348,11 +410,20 @@ class LocalSessionStore {
348
410
  turns[turnIndex].tools_used = toolsUsed;
349
411
  if (filesReferenced)
350
412
  turns[turnIndex].files_referenced = filesReferenced;
351
- // Rewrite the file atomically
413
+ // Ensure instance fields
414
+ if (!turns[turnIndex].instance_id) {
415
+ turns[turnIndex].instance_id = this.instanceId;
416
+ }
417
+ if (!turns[turnIndex].session_id) {
418
+ turns[turnIndex].session_id = sessionId;
419
+ }
420
+ // Rewrite the file atomically (always to instance-scoped path)
421
+ const instanceTurnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
422
+ (0, paths_js_1.ensureInstanceDir)(this.instanceId);
352
423
  const newContent = turns.map((t) => JSON.stringify(t)).join('\n') + '\n';
353
- const tmpPath = `${turnsPath}.tmp`;
424
+ const tmpPath = `${instanceTurnsPath}.tmp`;
354
425
  fs.writeFileSync(tmpPath, newContent, 'utf-8');
355
- fs.renameSync(tmpPath, turnsPath);
426
+ fs.renameSync(tmpPath, instanceTurnsPath);
356
427
  return {
357
428
  success: true,
358
429
  source: 'local',
@@ -390,12 +461,14 @@ class LocalSessionStore {
390
461
  if (ackedTurnId > meta.acked_turn_id) {
391
462
  meta.acked_turn_id = ackedTurnId;
392
463
  meta.last_flush_ts = new Date().toISOString();
464
+ meta.instance_id = this.instanceId;
393
465
  this.writeSessionMeta(meta);
394
466
  // Update index too
395
467
  const index = this.readIndex();
396
468
  const sessionName = Object.keys(index).find((name) => index[name].session_id === sessionId);
397
469
  if (sessionName) {
398
470
  index[sessionName].acked_turn_id = ackedTurnId;
471
+ index[sessionName].instance_id = this.instanceId;
399
472
  this.writeIndex(index);
400
473
  }
401
474
  }
@@ -435,6 +508,7 @@ class LocalSessionStore {
435
508
  if (ackedTurnId > currentSupabaseAck) {
436
509
  meta.supabase_acked_turn_id = ackedTurnId;
437
510
  meta.last_flush_ts = new Date().toISOString();
511
+ meta.instance_id = this.instanceId;
438
512
  this.writeSessionMeta(meta);
439
513
  }
440
514
  return {
@@ -459,7 +533,7 @@ class LocalSessionStore {
459
533
  */
460
534
  prune(sessionId) {
461
535
  const startTime = Date.now();
462
- const turnsPath = this.getTurnsPath(sessionId);
536
+ const turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
463
537
  try {
464
538
  const meta = this.getSessionMeta(sessionId);
465
539
  if (!meta) {
@@ -532,7 +606,7 @@ class LocalSessionStore {
532
606
  // SESSION MANAGEMENT
533
607
  // ═══════════════════════════════════════════════════════════════════════════
534
608
  /**
535
- * List all sessions, sorted by last active (newest first)
609
+ * List all sessions for current instance, sorted by last active (newest first)
536
610
  */
537
611
  listSessions() {
538
612
  const index = this.readIndex();
@@ -553,14 +627,47 @@ class LocalSessionStore {
553
627
  return entries;
554
628
  }
555
629
  /**
556
- * Get session ID from session name
630
+ * List all sessions across ALL instances (for global queries)
631
+ */
632
+ listAllSessions() {
633
+ const allEntries = [];
634
+ const instanceIds = (0, paths_js_1.listInstanceIds)();
635
+ for (const instId of instanceIds) {
636
+ const indexPath = (0, paths_js_1.getIndexPath)(instId);
637
+ try {
638
+ if (fs.existsSync(indexPath)) {
639
+ const content = fs.readFileSync(indexPath, 'utf-8');
640
+ const index = JSON.parse(content);
641
+ for (const [sessionName, entry] of Object.entries(index)) {
642
+ allEntries.push({
643
+ session_name: sessionName,
644
+ session_id: entry.session_id,
645
+ instance_id: instId,
646
+ last_active_ts: entry.last_active_ts,
647
+ turn_count: entry.last_turn_id,
648
+ project_path: entry.project_path,
649
+ is_current: false,
650
+ });
651
+ }
652
+ }
653
+ }
654
+ catch {
655
+ // Ignore errors for individual instances
656
+ }
657
+ }
658
+ // Sort by last active (newest first)
659
+ allEntries.sort((a, b) => new Date(b.last_active_ts).getTime() - new Date(a.last_active_ts).getTime());
660
+ return allEntries;
661
+ }
662
+ /**
663
+ * Get session ID from session name (searches current instance)
557
664
  */
558
665
  getSessionId(sessionName) {
559
666
  const index = this.readIndex();
560
667
  return index[sessionName]?.session_id || null;
561
668
  }
562
669
  /**
563
- * Get session name from session ID
670
+ * Get session name from session ID (searches current instance)
564
671
  */
565
672
  getSessionName(sessionId) {
566
673
  const index = this.readIndex();
@@ -572,7 +679,7 @@ class LocalSessionStore {
572
679
  return null;
573
680
  }
574
681
  /**
575
- * Check if a session exists in local cache
682
+ * Check if a session exists in local cache (current instance or legacy)
576
683
  */
577
684
  hasSession(sessionIdOrName) {
578
685
  const index = this.readIndex();
@@ -584,6 +691,10 @@ class LocalSessionStore {
584
691
  if (entry.session_id === sessionIdOrName)
585
692
  return true;
586
693
  }
694
+ // Check legacy paths
695
+ const legacyTurns = (0, paths_js_1.getLegacyTurnsPath)(sessionIdOrName);
696
+ if (fs.existsSync(legacyTurns))
697
+ return true;
587
698
  return false;
588
699
  }
589
700
  /**
@@ -592,9 +703,9 @@ class LocalSessionStore {
592
703
  deleteSession(sessionId) {
593
704
  const startTime = Date.now();
594
705
  try {
595
- // Remove files
596
- const turnsPath = this.getTurnsPath(sessionId);
597
- const metaPath = this.getMetaPath(sessionId);
706
+ // Remove instance-scoped files
707
+ const turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
708
+ const metaPath = (0, paths_js_1.getMetaPath)(this.instanceId, sessionId);
598
709
  if (fs.existsSync(turnsPath))
599
710
  fs.unlinkSync(turnsPath);
600
711
  if (fs.existsSync(metaPath))
@@ -648,7 +759,7 @@ class LocalSessionStore {
648
759
  // UTILITIES
649
760
  // ═══════════════════════════════════════════════════════════════════════════
650
761
  /**
651
- * Get cache statistics
762
+ * Get cache statistics for current instance
652
763
  */
653
764
  getStats() {
654
765
  const sessions = this.listSessions();
@@ -656,7 +767,7 @@ class LocalSessionStore {
656
767
  let cacheSize = 0;
657
768
  for (const session of sessions) {
658
769
  totalTurns += session.turn_count;
659
- const turnsPath = this.getTurnsPath(session.session_id);
770
+ const turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, session.session_id);
660
771
  try {
661
772
  const stats = fs.statSync(turnsPath);
662
773
  cacheSize += stats.size;
@@ -669,10 +780,11 @@ class LocalSessionStore {
669
780
  session_count: sessions.length,
670
781
  total_turns: totalTurns,
671
782
  cache_size_bytes: cacheSize,
783
+ instance_id: this.instanceId,
672
784
  };
673
785
  }
674
786
  /**
675
- * Clear all local cache (use with caution)
787
+ * Clear all local cache for current instance (use with caution)
676
788
  */
677
789
  clearAll() {
678
790
  const sessions = this.listSessions();
@@ -684,5 +796,9 @@ class LocalSessionStore {
684
796
  }
685
797
  }
686
798
  exports.LocalSessionStore = LocalSessionStore;
687
- // Export singleton instance
799
+ // Export factory function instead of singleton (to support instance-scoped usage)
800
+ function createLocalSessionStore(instanceId) {
801
+ return new LocalSessionStore({}, instanceId);
802
+ }
803
+ // Export default instance using EKKOS_INSTANCE_ID from env
688
804
  exports.localCache = new LocalSessionStore();
@@ -1,24 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * ekkOS Fast Capture & Restore - CLI for local cache operations
3
+ * ekkOS Fast Capture & Restore - CLI for local cache operations (Instance-Aware)
4
4
  *
5
- * Capture Commands:
5
+ * Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
6
+ * - All Tier 0 cache paths MUST be: ~/.ekkos/cache/sessions/{instanceId}/{sessionId}.jsonl
7
+ * - All persisted records MUST include: instanceId, sessionId, sessionName
8
+ *
9
+ * Capture Commands (NEW format with instanceId):
10
+ * capture user <instance_id> <session_id> <session_name> <turn_id> <query> [project_path]
11
+ * capture response <instance_id> <session_id> <turn_id> <response> [tools] [files]
12
+ *
13
+ * Capture Commands (LEGACY format - backward compatible, uses default instanceId):
6
14
  * capture user <session_id> <session_name> <turn_id> <query> [project_path]
7
15
  * capture response <session_id> <turn_id> <response> [tools] [files]
8
16
  *
9
17
  * Restore Commands:
10
- * capture restore [session_name] [--json|--markdown|--n=N]
18
+ * capture restore [session_name] [--json|--markdown|--n=N] [--instance=ID]
11
19
  *
12
- * ACK & Sync Commands (Phase 5):
13
- * capture ack <session_id> <turn_id> - Update ACK cursor after Redis success
14
- * capture sync [session_id] - Sync unACKed turns to Redis
15
- * capture prune <session_id> - Remove safely ACKed turns
16
- * capture cleanup - Prune all + evict old sessions
20
+ * ACK & Sync Commands:
21
+ * capture ack <session_id> <turn_id> [--instance=ID]
22
+ * capture sync [session_id] [--instance=ID]
23
+ * capture prune <session_id> [--instance=ID]
24
+ * capture cleanup [--instance=ID]
17
25
  *
18
26
  * Query Commands:
19
- * capture list - List all cached sessions
20
- * capture get <session_id> [n] - Get last N turns
21
- * capture stats - Cache statistics
27
+ * capture list [--instance=ID|--all]
28
+ * capture get <session_id> [n] [--instance=ID]
29
+ * capture stats [--instance=ID]
22
30
  *
23
31
  * This is a lightweight script designed for hook integration.
24
32
  * Writes to local JSONL cache with minimal latency.