@ekkos/cli 1.0.12 → 1.0.14

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.
@@ -301,25 +301,36 @@ function resolveJsonlPath(sessionName, createdAfterMs) {
301
301
  * This prevents picking up old sessions that are still being modified.
302
302
  */
303
303
  function findLatestJsonl(projectPath, createdAfterMs) {
304
- // Claude encodes project paths by replacing ALL separators (/ and \) with -.
305
- // On Windows projectPath uses backslashes, so we must normalize both.
306
- const encoded = projectPath.replace(/[\\/]/g, '-');
307
- const projectDir = path.join(os.homedir(), '.claude', 'projects', encoded);
308
- if (!fs.existsSync(projectDir))
309
- return null;
310
- const jsonlFiles = fs.readdirSync(projectDir)
311
- .filter(f => f.endsWith('.jsonl'))
312
- .map(f => {
313
- const stat = fs.statSync(path.join(projectDir, f));
314
- return {
315
- path: path.join(projectDir, f),
316
- mtime: stat.mtimeMs,
317
- birthtime: stat.birthtimeMs,
318
- };
319
- })
320
- .filter(f => !createdAfterMs || f.birthtime > createdAfterMs)
321
- .sort((a, b) => b.mtime - a.mtime);
322
- return jsonlFiles.length > 0 ? jsonlFiles[0].path : null;
304
+ // Claude encodes project paths by replacing separators with '-'.
305
+ // On Windows, ':' is also illegal in directory names so it gets replaced too.
306
+ // Try all plausible encodings since Claude's exact scheme varies by platform.
307
+ const candidateEncodings = new Set([
308
+ projectPath.replace(/[\\/]/g, '-'), // C:-Users-name (backslash only)
309
+ projectPath.replace(/[:\\/]/g, '-'), // C--Users-name (colon + backslash)
310
+ '-' + projectPath.replace(/[:\\/]/g, '-'), // -C--Users-name (leading separator)
311
+ projectPath.replace(/\//g, '-'), // macOS: /Users/name → -Users-name
312
+ ]);
313
+ const projectsRoot = path.join(os.homedir(), '.claude', 'projects');
314
+ for (const encoded of candidateEncodings) {
315
+ const projectDir = path.join(projectsRoot, encoded);
316
+ if (!fs.existsSync(projectDir))
317
+ continue;
318
+ const jsonlFiles = fs.readdirSync(projectDir)
319
+ .filter(f => f.endsWith('.jsonl'))
320
+ .map(f => {
321
+ const stat = fs.statSync(path.join(projectDir, f));
322
+ return {
323
+ path: path.join(projectDir, f),
324
+ mtime: stat.mtimeMs,
325
+ birthtime: stat.birthtimeMs,
326
+ };
327
+ })
328
+ .filter(f => !createdAfterMs || f.birthtime > createdAfterMs)
329
+ .sort((a, b) => b.mtime - a.mtime);
330
+ if (jsonlFiles.length > 0)
331
+ return jsonlFiles[0].path;
332
+ }
333
+ return null;
323
334
  }
324
335
  function getLatestSession() {
325
336
  const sessions = (0, state_js_1.getActiveSessions)();
@@ -351,6 +362,28 @@ async function waitForNewSession() {
351
362
  const startWait = Date.now();
352
363
  let candidateName = null;
353
364
  while (Date.now() - startWait < maxWaitMs) {
365
+ // ── Windows hook hint file ──────────────────────────────────────────────
366
+ // On Windows, active-sessions.json is never populated because hook processes
367
+ // are short-lived and their PIDs are dead by the time we poll. Instead, the
368
+ // user-prompt-submit.ps1 hook writes ~/.ekkos/hook-session-hint.json with
369
+ // { sessionName, sessionId, projectPath, ts } on every turn. Read it here.
370
+ const hintPath = path.join(state_js_1.EKKOS_DIR, 'hook-session-hint.json');
371
+ try {
372
+ if (fs.existsSync(hintPath)) {
373
+ const hint = JSON.parse(fs.readFileSync(hintPath, 'utf-8'));
374
+ if (hint.ts >= launchTs - 5000 && hint.sessionName && hint.projectPath) {
375
+ candidateName = hint.sessionName;
376
+ const jsonlPath = findLatestJsonl(hint.projectPath, launchTs)
377
+ || resolveJsonlPath(hint.sessionName, launchTs);
378
+ if (jsonlPath) {
379
+ console.log(chalk_1.default.green(` Found session (hook hint): ${hint.sessionName}`));
380
+ return { sessionName: hint.sessionName, jsonlPath };
381
+ }
382
+ }
383
+ }
384
+ }
385
+ catch { /* ignore */ }
386
+ // ── Standard: active-sessions.json (works on Mac/Linux) ────────────────
354
387
  const sessions = (0, state_js_1.getActiveSessions)();
355
388
  // Find sessions that started after our launch
356
389
  for (const s of sessions) {
@@ -384,6 +417,40 @@ async function waitForNewSession() {
384
417
  return { sessionName: name, jsonlPath: latestJsonl };
385
418
  }
386
419
  }
420
+ // Broad fallback: scan ALL project directories for any new JSONL (Windows safety net).
421
+ // Claude creates the JSONL immediately when a session starts, before the first message.
422
+ // This catches cases where path encoding doesn't match.
423
+ {
424
+ const projectsRoot = path.join(os.homedir(), '.claude', 'projects');
425
+ try {
426
+ if (fs.existsSync(projectsRoot)) {
427
+ const allNewJsonl = fs.readdirSync(projectsRoot)
428
+ .flatMap(dir => {
429
+ const dirPath = path.join(projectsRoot, dir);
430
+ try {
431
+ return fs.readdirSync(dirPath)
432
+ .filter(f => f.endsWith('.jsonl'))
433
+ .map(f => {
434
+ const fp = path.join(dirPath, f);
435
+ const stat = fs.statSync(fp);
436
+ return { path: fp, birthtime: stat.birthtimeMs, mtime: stat.mtimeMs };
437
+ })
438
+ .filter(f => f.birthtime > launchTs);
439
+ }
440
+ catch {
441
+ return [];
442
+ }
443
+ })
444
+ .sort((a, b) => b.birthtime - a.birthtime);
445
+ if (allNewJsonl.length > 0) {
446
+ const name = candidateName || 'session';
447
+ console.log(chalk_1.default.green(` Found session via scan: ${name}`));
448
+ return { sessionName: name, jsonlPath: allNewJsonl[0].path };
449
+ }
450
+ }
451
+ }
452
+ catch { /* ignore */ }
453
+ }
387
454
  await sleep(pollMs);
388
455
  process.stdout.write(chalk_1.default.gray('.'));
389
456
  }
@@ -187,6 +187,10 @@ const transcript_repair_1 = require("../capture/transcript-repair");
187
187
  let pty = null;
188
188
  let ptyLoadPromise = null;
189
189
  async function loadPty() {
190
+ // node-pty uses native addons that don't load cleanly on Windows.
191
+ // All PTY code paths already guard with `!isWindows`, so skip the import entirely.
192
+ if (isWindows)
193
+ return null;
190
194
  if (pty)
191
195
  return pty;
192
196
  if (!ptyLoadPromise) {
@@ -843,14 +847,16 @@ function launchWithDashboardWindows(options) {
843
847
  // Use single-quoted inner args so backslashes in Windows paths survive PowerShell expansion.
844
848
  // The wt command runs each pane as: powershell -NoExit -Command "& 'node' 'C:\path\ekkos' ..."
845
849
  const ekkosCmdEscaped = ekkosCmd.replace(/'/g, "''"); // escape single quotes in path
846
- const runPsCmd = `& node '${ekkosCmdEscaped}' ${runArgs.join(' ')}`;
850
+ const cwdEscaped = cwd.replace(/'/g, "''");
851
+ // cd to original CWD first so ekkos run --kickstart registers the correct projectPath
852
+ const runPsCmd = `Set-Location '${cwdEscaped}'; & node '${ekkosCmdEscaped}' ${runArgs.join(' ')}`;
847
853
  const dashPsCmd = `& node '${ekkosCmdEscaped}' dashboard --wait-for-new --refresh 2000`;
848
854
  // Windows Terminal split pane command:
849
- // wt --window 0 split-pane -V --size 0.4 --title "ekkOS Dashboard" powershell -NoExit -Command "..."
850
- // If no WT window exists yet, open a new one with two panes
855
+ // wt new-tab --startingDirectory "..." powershell -NoExit -Command "..."
856
+ // Passing --startingDirectory ensures both panes inherit the correct project CWD.
851
857
  const wtCmd = [
852
858
  'wt',
853
- `new-tab --title "ekkOS" powershell -NoExit -Command "${runPsCmd}"`,
859
+ `new-tab --startingDirectory "${cwd}" --title "ekkOS" powershell -NoExit -Command "${runPsCmd}"`,
854
860
  `; split-pane -V --size 0.4 --title "ekkOS Dashboard" powershell -NoExit -Command "${dashPsCmd}"`
855
861
  ].join(' ');
856
862
  try {
@@ -79,7 +79,8 @@ function isEkkosSessionName(name) {
79
79
  return /^[a-z]+-[a-z]+-[a-z]+$/.test(name);
80
80
  }
81
81
  function encodeProjectPath(projectPath) {
82
- return projectPath.replace(/\//g, '-');
82
+ // Replace all path separators (and Windows drive colon) with '-'
83
+ return projectPath.replace(/[:\\/]/g, '-');
83
84
  }
84
85
  /** Resolve an ekkOS session name to a JSONL UUID */
85
86
  function resolveSessionName(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -260,6 +260,25 @@ if (Test-Path $configFile) {
260
260
 
261
261
  $timestamp = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt") + " EST"
262
262
 
263
+ # ═══════════════════════════════════════════════════════════════════════════
264
+ # DASHBOARD HINT FILE: write session info for ekkos dashboard --wait-for-new
265
+ # On Windows, active-sessions.json is never populated (hook PIDs are dead).
266
+ # The dashboard reads this file instead to locate the JSONL path.
267
+ # ═══════════════════════════════════════════════════════════════════════════
268
+ if ($sessionName -ne "unknown-session") {
269
+ try {
270
+ $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
271
+ $hintFile = Join-Path $EkkosConfigDir "hook-session-hint.json"
272
+ $hint = @{
273
+ sessionName = $sessionName
274
+ sessionId = $rawSessionId
275
+ projectPath = $projectPath
276
+ ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
277
+ } | ConvertTo-Json -Compress
278
+ Set-Content -Path $hintFile -Value $hint -Force
279
+ } catch {}
280
+ }
281
+
263
282
  # ═══════════════════════════════════════════════════════════════════════════
264
283
  # OUTPUT SYSTEM REMINDER
265
284
  # ═══════════════════════════════════════════════════════════════════════════