session-steward 0.5.2 → 0.6.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0] - 2026-08-16
4
+
5
+ ### Added
6
+
7
+ - Windows support, alongside macOS and Linux. Codex and Claude Code sessions are read from your Windows user profile, and Claude Desktop data is found in both the standalone and Microsoft Store locations.
8
+ - Inside WSL, Session Steward manages the sessions stored in your Linux home folder. Run it from Windows to manage the sessions in your Windows profile.
9
+
3
10
  ## [0.5.2] - 2026-08-13
4
11
 
5
12
  ### Changed
@@ -105,6 +112,7 @@
105
112
  - Support for custom Codex home folders and a saved folder preference.
106
113
  - Streaming and bounded-memory discovery for large session collections and transcripts.
107
114
 
115
+ [0.6.0]: https://github.com/mallikcheripally/session-steward/compare/v0.5.2...v0.6.0
108
116
  [0.5.2]: https://github.com/mallikcheripally/session-steward/compare/v0.5.1...v0.5.2
109
117
  [0.5.1]: https://github.com/mallikcheripally/session-steward/compare/v0.5.0...v0.5.1
110
118
  [0.5.0]: https://github.com/mallikcheripally/session-steward/compare/v0.4.0...v0.5.0
package/README.md CHANGED
@@ -47,7 +47,7 @@ At startup, Session Steward may contact the public npm registry to check for a n
47
47
 
48
48
  ## Install and get started
49
49
 
50
- Session Steward supports macOS and Linux and requires Node.js 24.15 or newer. Git and a separate SQLite installation are not required.
50
+ Session Steward supports macOS, Linux, and Windows and requires Node.js 24.15 or newer.
51
51
 
52
52
  Install it globally:
53
53
 
@@ -67,7 +67,9 @@ Or try it without installing:
67
67
  npx session-steward@latest
68
68
  ```
69
69
 
70
- Session Steward opens in your browser, listens only on `127.0.0.1`, and detects `~/.codex` and `~/.claude` by default. Claude Desktop sessions are detected on macOS; Claude Code CLI sessions work on macOS and Linux.
70
+ Session Steward opens in your browser, listens only on `127.0.0.1`, and detects `~/.codex` and `~/.claude` by default. Claude Code CLI and local Claude Desktop sessions are detected on macOS and Windows; the Claude Code CLI is also supported on Linux. On Windows, these resolve to `%USERPROFILE%\.codex` and `%USERPROFILE%\.claude`.
71
+
72
+ When run inside WSL, Session Steward uses the Linux home folder and manages sessions stored there. Run it from Windows to manage sessions in your Windows profile.
71
73
 
72
74
  To clean up sessions:
73
75
 
@@ -271,7 +273,7 @@ Results vary with hardware, disk speed, and session layout. Tests and benchmarks
271
273
 
272
274
  ## Support
273
275
 
274
- Codex, Claude Code CLI, and local Claude Code Desktop sessions are supported. Claude Desktop archive is not treated as deletion, and Session Steward never removes its worktrees.
276
+ Codex, Claude Code CLI, and local Claude Code Desktop sessions are supported. Claude Desktop archive is not treated as deletion, and Session Steward never removes its worktrees. On Windows, both the standalone and Microsoft Store Claude Desktop data locations are detected.
275
277
 
276
278
  Use [GitHub Issues](https://github.com/mallikcheripally/session-steward/issues) to report a bug, request a provider, or share a storage format that Session Steward does not recognize.
277
279
 
@@ -4,6 +4,7 @@ import { parseArgs } from "node:util";
4
4
  import { spawn } from "node:child_process";
5
5
 
6
6
  import packageMetadata from "../package.json" with { type: "json" };
7
+ import { getBrowserOpenInvocation } from "../lib/platform.mjs";
7
8
  import { assertSupportedNode } from "../lib/runtime.mjs";
8
9
  import { findAvailableUpdate, formatUpdateNotice } from "../lib/update-check.mjs";
9
10
 
@@ -58,14 +59,14 @@ process.stdout.write(`Session Steward is running at http://127.0.0.1:${server.po
58
59
 
59
60
  if (!values["no-open"]) {
60
61
  const url = `http://127.0.0.1:${server.port}`;
61
- const openerCommand = process.platform === "darwin"
62
- ? "open"
63
- : process.platform === "linux"
64
- ? "xdg-open"
65
- : null;
62
+ const invocation = getBrowserOpenInvocation(url);
66
63
 
67
- if (openerCommand) {
68
- const opener = spawn(openerCommand, [url], { detached: true, stdio: "ignore" });
64
+ if (invocation) {
65
+ const opener = spawn(invocation.command, invocation.args, {
66
+ detached: true,
67
+ stdio: "ignore",
68
+ windowsHide: invocation.windowsHide,
69
+ });
69
70
  opener.unref();
70
71
  } else {
71
72
  process.stdout.write(`Open ${url} in a browser.\n`);
@@ -0,0 +1,178 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ const WINDOWS_ROOTED = /^(?:[a-z]:[\\/]|[\\/]{2})/iu;
6
+
7
+ function pathApi(platform) {
8
+ if (platform === "win32") {
9
+ return path.win32;
10
+ }
11
+
12
+ if (platform === "darwin" || platform === "linux") {
13
+ return path.posix;
14
+ }
15
+
16
+ return path;
17
+ }
18
+
19
+ function isUsableAbsolutePath(value, platform) {
20
+ if (typeof value !== "string" || !pathApi(platform).isAbsolute(value)) {
21
+ return false;
22
+ }
23
+
24
+ // path.win32.isAbsolute accepts root-relative paths such as "/home/user",
25
+ // which resolve against whichever drive happens to be current.
26
+ return platform !== "win32" || WINDOWS_ROOTED.test(value);
27
+ }
28
+
29
+ export function expandHomePath(value, {
30
+ home = os.homedir(),
31
+ platform = process.platform,
32
+ } = {}) {
33
+ if (value === "~") {
34
+ return home;
35
+ }
36
+
37
+ if (typeof value === "string" && /^~[\\/]/u.test(value)) {
38
+ return pathApi(platform).join(home, value.slice(2));
39
+ }
40
+
41
+ return value;
42
+ }
43
+
44
+ export function getDefaultConfigDirectory({
45
+ env = process.env,
46
+ home = os.homedir(),
47
+ platform = process.platform,
48
+ } = {}) {
49
+ const paths = pathApi(platform);
50
+ const xdgConfigHome = env.XDG_CONFIG_HOME;
51
+
52
+ if (isUsableAbsolutePath(xdgConfigHome, platform)) {
53
+ return paths.join(xdgConfigHome, "session-steward");
54
+ }
55
+
56
+ if (platform === "darwin") {
57
+ return paths.join(home, "Library", "Application Support", "session-steward");
58
+ }
59
+
60
+ if (platform === "win32") {
61
+ const appData = env.APPDATA;
62
+ return isUsableAbsolutePath(appData, platform)
63
+ ? paths.join(appData, "session-steward")
64
+ : paths.join(home, "AppData", "Roaming", "session-steward");
65
+ }
66
+
67
+ return paths.join(home, ".config", "session-steward");
68
+ }
69
+
70
+ const desktopDataHomeCache = new Map();
71
+
72
+ export function invalidateClaudeDesktopDataHome() {
73
+ desktopDataHomeCache.clear();
74
+ }
75
+
76
+ function resolveClaudeDesktopDataHome({ env, fileSystem, home, platform }) {
77
+ const paths = pathApi(platform);
78
+
79
+ if (platform === "darwin") {
80
+ return paths.join(home, "Library", "Application Support", "Claude");
81
+ }
82
+
83
+ if (platform === "win32") {
84
+ const appData = env.APPDATA;
85
+ const standardHome = isUsableAbsolutePath(appData, platform)
86
+ ? paths.join(appData, "Claude")
87
+ : paths.join(home, "AppData", "Roaming", "Claude");
88
+ const sessionsName = "claude-code-sessions";
89
+
90
+ if (fileSystem.existsSync(paths.join(standardHome, sessionsName))) {
91
+ return standardHome;
92
+ }
93
+
94
+ const localAppData = env.LOCALAPPDATA;
95
+ if (isUsableAbsolutePath(localAppData, platform)) {
96
+ const packagesDirectory = paths.join(localAppData, "Packages");
97
+ try {
98
+ const packageEntries = fileSystem.readdirSync(packagesDirectory, { withFileTypes: true });
99
+ for (const entry of packageEntries) {
100
+ if (!entry.isDirectory() || !/^Claude_/iu.test(entry.name)) {
101
+ continue;
102
+ }
103
+
104
+ const packageHome = paths.join(
105
+ packagesDirectory,
106
+ entry.name,
107
+ "LocalCache",
108
+ "Roaming",
109
+ "Claude",
110
+ );
111
+ if (fileSystem.existsSync(paths.join(packageHome, sessionsName))) {
112
+ return packageHome;
113
+ }
114
+ }
115
+ } catch {
116
+ }
117
+ }
118
+
119
+ return standardHome;
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ export function getClaudeDesktopDataHome({
126
+ env = process.env,
127
+ fileSystem = { existsSync, readdirSync },
128
+ home = os.homedir(),
129
+ platform = process.platform,
130
+ } = {}) {
131
+ // Windows discovery reads the file system, and this runs on every listing.
132
+ const key = `${platform}\0${home}\0${env.APPDATA || ""}\0${env.LOCALAPPDATA || ""}`;
133
+ if (desktopDataHomeCache.has(key)) {
134
+ return desktopDataHomeCache.get(key);
135
+ }
136
+
137
+ const resolved = resolveClaudeDesktopDataHome({ env, fileSystem, home, platform });
138
+ desktopDataHomeCache.set(key, resolved);
139
+ return resolved;
140
+ }
141
+
142
+ export function getBrowserOpenInvocation(url, {
143
+ env = process.env,
144
+ platform = process.platform,
145
+ } = {}) {
146
+ if (platform === "darwin") {
147
+ return { args: [url], command: "open" };
148
+ }
149
+
150
+ if (platform === "linux") {
151
+ return { args: [url], command: "xdg-open" };
152
+ }
153
+
154
+ if (platform === "win32") {
155
+ return {
156
+ args: ["/d", "/s", "/c", "start", "", url],
157
+ command: env.ComSpec || env.COMSPEC || "cmd.exe",
158
+ windowsHide: true,
159
+ };
160
+ }
161
+
162
+ return null;
163
+ }
164
+
165
+ export function getCommandInvocation(command, args, {
166
+ env = process.env,
167
+ platform = process.platform,
168
+ } = {}) {
169
+ if (platform !== "win32") {
170
+ return { args, command };
171
+ }
172
+
173
+ return {
174
+ args: ["/d", "/s", "/c", command, ...args],
175
+ command: env.ComSpec || env.COMSPEC || "cmd.exe",
176
+ windowsHide: true,
177
+ };
178
+ }
@@ -2,10 +2,14 @@ import { createHash, randomBytes } from "node:crypto";
2
2
  import { once } from "node:events";
3
3
  import { createReadStream, createWriteStream } from "node:fs";
4
4
  import { promises as fs } from "node:fs";
5
- import os from "node:os";
6
5
  import path from "node:path";
7
6
  import { finished, pipeline } from "node:stream/promises";
8
7
 
8
+ import {
9
+ expandHomePath,
10
+ getClaudeDesktopDataHome,
11
+ invalidateClaudeDesktopDataHome,
12
+ } from "../../platform.mjs";
9
13
  import { measurePath } from "../../storage/files.mjs";
10
14
  import { readJsonlEntries, rewriteJsonlFile } from "../../storage/jsonl.mjs";
11
15
 
@@ -34,19 +38,11 @@ const SESSION_SORTS = new Set(["created", "cwd", "name", "size", "updated"]);
34
38
  const discoveryCache = new Map();
35
39
  const transcriptActivityCache = new Map();
36
40
 
37
- function expandHome(value) {
38
- if (value === "~") return os.homedir();
39
- if (value?.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
40
- return value;
41
- }
42
-
43
41
  function getPaths(claudeHomeInput, desktopDataHomeInput) {
44
- const claudeHome = path.resolve(expandHome(claudeHomeInput || process.env.CLAUDE_CONFIG_DIR || "~/.claude"));
42
+ const claudeHome = path.resolve(expandHomePath(claudeHomeInput || process.env.CLAUDE_CONFIG_DIR || "~/.claude"));
45
43
  const desktopDataHome = desktopDataHomeInput
46
- ? path.resolve(expandHome(desktopDataHomeInput))
47
- : process.platform === "darwin"
48
- ? path.join(os.homedir(), "Library", "Application Support", "Claude")
49
- : null;
44
+ ? path.resolve(expandHomePath(desktopDataHomeInput))
45
+ : getClaudeDesktopDataHome();
50
46
  return {
51
47
  backupRoot: path.join(claudeHome, "session-steward-backups"),
52
48
  claudeHome,
@@ -401,6 +397,9 @@ async function discover(claudeHome, desktopDataHome) {
401
397
  }
402
398
 
403
399
  async function discoverCached(claudeHome, desktopDataHome, { refresh = false } = {}) {
400
+ if (refresh && !desktopDataHome) {
401
+ invalidateClaudeDesktopDataHome();
402
+ }
404
403
  const paths = getPaths(claudeHome, desktopDataHome);
405
404
  const key = `${paths.claudeHome}\0${paths.desktopDataHome || ""}`;
406
405
  const cached = discoveryCache.get(key);
@@ -416,6 +415,7 @@ async function discoverCached(claudeHome, desktopDataHome, { refresh = false } =
416
415
  export function invalidateSessionCache({ claudeHome, desktopDataHome }) {
417
416
  const paths = getPaths(claudeHome, desktopDataHome);
418
417
  discoveryCache.delete(`${paths.claudeHome}\0${paths.desktopDataHome || ""}`);
418
+ invalidateClaudeDesktopDataHome();
419
419
  }
420
420
 
421
421
  function filterRecords(records, options) {
@@ -1,7 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { constants as fsConstants, createReadStream } from "node:fs";
3
3
  import { promises as fs } from "node:fs";
4
- import os from "node:os";
5
4
  import path from "node:path";
6
5
  import readline from "node:readline";
7
6
 
@@ -25,18 +24,7 @@ import {
25
24
  invalidateCodexDatabaseResolution,
26
25
  resolveCodexDatabases,
27
26
  } from "./database-families.mjs";
28
-
29
- function expandHome(value) {
30
- if (!value || value === "~") {
31
- return os.homedir();
32
- }
33
-
34
- if (value.startsWith("~/")) {
35
- return path.join(os.homedir(), value.slice(2));
36
- }
37
-
38
- return value;
39
- }
27
+ import { expandHomePath } from "../../platform.mjs";
40
28
 
41
29
  function normalizeText(value) {
42
30
  if (typeof value !== "string") {
@@ -448,7 +436,7 @@ function deriveDisplayName({
448
436
  }
449
437
 
450
438
  export function getCodexPaths(codexHomeInput, { refresh = false } = {}) {
451
- const codexHome = path.resolve(expandHome(codexHomeInput || "~/.codex"));
439
+ const codexHome = path.resolve(expandHomePath(codexHomeInput || "~/.codex"));
452
440
  const archivedSessionsDirectory = path.join(codexHome, "archived_sessions");
453
441
  const sessionsDirectory = path.join(codexHome, "sessions");
454
442
  const resolution = resolveCodexDatabases(codexHome, { refresh });
@@ -1017,7 +1005,7 @@ async function getSessionSizeIndex(paths, { refresh = false } = {}) {
1017
1005
  }
1018
1006
 
1019
1007
  export function invalidateSessionCache({ codexHome }) {
1020
- const resolvedHome = path.resolve(expandHome(codexHome || "~/.codex"));
1008
+ const resolvedHome = path.resolve(expandHomePath(codexHome || "~/.codex"));
1021
1009
  sessionSizeCache.delete(resolvedHome);
1022
1010
  invalidateCodexDatabaseResolution(resolvedHome);
1023
1011
  }
package/lib/server.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  } from "./session-event-reader.mjs";
13
13
  import { createProviderSettings } from "./settings.mjs";
14
14
  import { classifyInstalledVersion } from "./version-support.mjs";
15
+ import { getCommandInvocation } from "./platform.mjs";
15
16
 
16
17
  const MAX_BODY_BYTES = 64 * 1024;
17
18
  const ALLOWED_SCOPES = new Set(["core", "deep"]);
@@ -32,7 +33,12 @@ const staticAssets = new Map([
32
33
 
33
34
  function readCommandVersion(command, args) {
34
35
  try {
35
- return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
36
+ const invocation = getCommandInvocation(command, args);
37
+ return execFileSync(invocation.command, invocation.args, {
38
+ encoding: "utf8",
39
+ stdio: ["ignore", "pipe", "ignore"],
40
+ windowsHide: invocation.windowsHide,
41
+ }).trim() || null;
36
42
  } catch {
37
43
  return null;
38
44
  }
package/lib/settings.mjs CHANGED
@@ -3,6 +3,11 @@ import { promises as fs } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
 
6
+ import {
7
+ expandHomePath,
8
+ getDefaultConfigDirectory as resolveDefaultConfigDirectory,
9
+ } from "./platform.mjs";
10
+
6
11
  const CONFIG_VERSION = 1;
7
12
  const PROVIDERS = {
8
13
  codex: {
@@ -21,24 +26,12 @@ const PROVIDERS = {
21
26
  },
22
27
  };
23
28
 
24
- function expandHome(value) {
25
- if (value === "~") {
26
- return os.homedir();
27
- }
28
-
29
- if (value.startsWith("~/")) {
30
- return path.join(os.homedir(), value.slice(2));
31
- }
32
-
33
- return value;
34
- }
35
-
36
29
  function normalizeHome(value) {
37
30
  if (typeof value !== "string" || value.includes("\0")) {
38
31
  throw new Error("Enter a valid folder path.");
39
32
  }
40
33
 
41
- const expanded = expandHome(value.trim());
34
+ const expanded = expandHomePath(value.trim());
42
35
 
43
36
  if (!expanded || !path.isAbsolute(expanded)) {
44
37
  throw new Error("Enter a full folder path, such as ~/.codex.");
@@ -58,17 +51,7 @@ function getProviderDefinition(providerId) {
58
51
  }
59
52
 
60
53
  export function getDefaultConfigDirectory() {
61
- const xdgConfigHome = process.env.XDG_CONFIG_HOME;
62
-
63
- if (xdgConfigHome && path.isAbsolute(xdgConfigHome)) {
64
- return path.join(xdgConfigHome, "session-steward");
65
- }
66
-
67
- if (process.platform === "darwin") {
68
- return path.join(os.homedir(), "Library", "Application Support", "session-steward");
69
- }
70
-
71
- return path.join(os.homedir(), ".config", "session-steward");
54
+ return resolveDefaultConfigDirectory();
72
55
  }
73
56
 
74
57
  async function readConfig(configPath) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "session-steward",
3
- "version": "0.5.2",
4
- "description": "A local Codex and Claude Code session manager for safely reviewing and deleting old sessions.",
3
+ "version": "0.6.0",
4
+ "description": "Codex and Claude Code session manager - browse, back up, and delete old sessions. Local browser UI + CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Mallik Cheripally",
7
7
  "type": "module",
@@ -17,28 +17,25 @@
17
17
  "bugs": "https://github.com/mallikcheripally/session-steward/issues",
18
18
  "keywords": [
19
19
  "codex",
20
- "claude-code",
21
- "claude-code-sessions",
22
- "claude-desktop",
23
20
  "codex-cli",
24
- "openai-codex",
25
21
  "codex-sessions",
26
- "codex-session-manager",
27
- "codex-session-cleanup",
28
22
  "codex-cleanup",
29
- "codex-history",
30
- "delete-codex-sessions",
31
- "session-manager",
32
- "session-cleanup",
33
- "ai-session-manager",
23
+ "openai-codex",
34
24
  "chatgpt-desktop",
35
- "chatgpt-history",
36
- "ai-coding-agent",
37
- "chatgpt-app-cleanup"
25
+ "claude-code",
26
+ "claude-code-sessions",
27
+ "claude-code-cleanup",
28
+ "claude-desktop",
29
+ "session-cleanup",
30
+ "session-history",
31
+ "session-manager",
32
+ "disk-space",
33
+ "disk-cleanup"
38
34
  ],
39
35
  "os": [
40
36
  "darwin",
41
- "linux"
37
+ "linux",
38
+ "win32"
42
39
  ],
43
40
  "files": [
44
41
  "bin",
@@ -56,6 +53,7 @@
56
53
  },
57
54
  "scripts": {
58
55
  "build": "vite build",
56
+ "benchmark:claude-discovery": "node --expose-gc test/benchmarks/claude-discovery.mjs",
59
57
  "benchmark:discovery": "node --expose-gc test/benchmarks/codex-discovery.mjs",
60
58
  "benchmark:events": "node --expose-gc test/benchmarks/session-events.mjs",
61
59
  "benchmark:overview": "node --expose-gc test/benchmarks/codex-overview.mjs",