contextswitch 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -1,100 +1,123 @@
1
1
  # ContextSwitch
2
2
 
3
- Domain-based context management for Claude Code CLI. Switch between different project contexts while preserving session history.
3
+ Manage domain-based contexts for Claude Code CLI. Switch between projects without losing session history -- each domain remembers where you left off via Claude's `--resume` flag.
4
4
 
5
- ## Features
5
+ ## Installation
6
6
 
7
- **Session Persistence** - Resume previous Claude sessions when switching domains
8
- ✅ **Domain Management** - Organize projects with separate configurations
9
- ✅ **MCP Server Support** - Configure Model Context Protocol servers per domain
10
- ✅ **Cross-Platform** - Works on macOS, Linux, and Windows
11
- ✅ **Session Archives** - Save and restore session snapshots
12
- ✅ **Process Management** - Clean switching between Claude instances
7
+ **Prerequisites**: Node.js 18 or later, [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
13
8
 
14
- ## Installation
9
+ ```bash
10
+ npm install -g contextswitch
11
+ ```
12
+
13
+ This installs the `cs` command globally.
14
+
15
+ ## Quick Start
15
16
 
16
- ### Quick Install
17
17
  ```bash
18
- # Clone the repository
19
- git clone https://github.com/yourusername/contextswitch.git
20
- cd contextswitch
18
+ # Initialize ContextSwitch (creates a default domain)
19
+ cs init
20
+
21
+ # Create a domain for a project
22
+ cs domain add backend --working-dir ~/projects/api
23
+
24
+ # Switch to that domain (launches Claude in the domain's working directory)
25
+ cs switch backend
21
26
 
22
- # Run the installation script
23
- chmod +x install.sh
24
- ./install.sh
27
+ # See all domains
28
+ cs list
25
29
  ```
26
30
 
27
- This sets up the `cs` command for easy use. For other installation methods, see [INSTALLATION.md](INSTALLATION.md).
31
+ ## Commands
28
32
 
29
- ### Manual Install
30
- ```bash
31
- # Install dependencies
32
- npm install
33
+ ### Core
33
34
 
34
- # Build the project
35
- npm run build:node
35
+ | Command | Description |
36
+ |---|---|
37
+ | `cs init` | Initialize ContextSwitch and create a default domain |
38
+ | `cs switch <domain>` | Switch to a domain (kills any running Claude, launches a new one) |
39
+ | `cs list` | List all domains (`ls` also works) |
40
+ | `cs status [domain]` | Show session info for the current or specified domain |
36
41
 
37
- # Add alias to your shell
38
- echo "alias cs='node $(pwd)/dist/cli.js'" >> ~/.zshrc
39
- source ~/.zshrc
42
+ **switch** accepts an optional flag:
40
43
 
41
- # Now you can use 'cs' command
42
- cs --help
44
+ ```bash
45
+ cs switch backend # Resume existing session if one exists
46
+ cs switch backend --force # Force a brand-new session
43
47
  ```
44
48
 
45
- ## Quick Start
49
+ ### Domain Management
50
+
51
+ | Command | Description |
52
+ |---|---|
53
+ | `cs domain add <name>` | Create a new domain |
54
+ | `cs domain remove <name>` | Delete a domain (`rm` also works) |
55
+ | `cs reset <domain>` | Reset a domain's session, archiving the current one first |
56
+ | `cs archive <domain>` | Snapshot the current session without resetting |
57
+
58
+ **domain add** options:
46
59
 
47
60
  ```bash
48
- # Initialize ContextSwitch
49
- node dist/cli.js init
61
+ # Unix
62
+ cs domain add backend --working-dir ~/projects/api
63
+ cs domain add frontend -w ~/projects/app --description "React frontend"
64
+
65
+ # Windows (Git Bash)
66
+ cs domain add backend --working-dir C:/Users/you/projects/api
67
+ cs domain add frontend -w C:/Users/you/projects/app -d "React frontend"
50
68
 
51
- # Create a new domain for your project (working directory is REQUIRED)
52
- node dist/cli.js domain add backend --working-dir ~/projects/api
69
+ # Inherit settings from another domain
70
+ cs domain add staging --extends backend
71
+ ```
53
72
 
54
- # The working directory is where Claude will operate when you switch to this domain
55
- node dist/cli.js switch backend
73
+ If `--working-dir` is omitted, the current directory is used.
56
74
 
57
- # Check status
58
- node dist/cli.js status
75
+ **reset** options:
59
76
 
60
- # List all domains
61
- node dist/cli.js list
77
+ ```bash
78
+ cs reset backend # Archive current session, then reset
79
+ cs reset backend --skip-archive # Reset without archiving
62
80
  ```
63
81
 
64
- ## Commands
82
+ ### Diagnostics
65
83
 
66
- ### Core Commands
84
+ | Command | Description |
85
+ |---|---|
86
+ | `cs doctor` | Check Claude CLI installation, config directory, running processes |
87
+ | `cs --help` | Show help |
88
+ | `cs --version` | Show version |
67
89
 
68
- - `cs init` - Initialize ContextSwitch configuration
69
- - `cs switch <domain>` - Switch to a different domain
70
- - `cs list` - List all available domains
71
- - `cs status [domain]` - Show current domain and session status
90
+ ## Platform Support
72
91
 
73
- ### Domain Management
92
+ | Platform | Status | Notes |
93
+ |---|---|---|
94
+ | macOS | Supported | Primary development platform |
95
+ | Linux | Supported | |
96
+ | Windows | Partial | Git Bash required; some process management features may not work |
74
97
 
75
- - `cs domain add <name>` - Create a new domain
76
- - `cs domain remove <name>` - Delete a domain
77
- - `cs reset <domain>` - Reset domain session (with auto-archive)
78
- - `cs archive <domain>` - Archive current session
98
+ Config and state are stored in the platform-appropriate location:
79
99
 
80
- ### Diagnostics
100
+ - macOS: `~/Library/Application Support/contextswitch/`
101
+ - Linux: `~/.config/contextswitch/`
102
+ - Windows: `%APPDATA%\contextswitch\`
81
103
 
82
- - `cs doctor` - Check system configuration
83
- - `cs --help` - Show help information
84
- - `cs --version` - Show version
104
+ ## How It Works
85
105
 
86
- ## Domain Configuration
106
+ **Domains** are named project contexts. Each domain has a working directory, an optional description, optional MCP server configuration, and a session history. Domain config is stored as a YAML file and can be edited directly.
87
107
 
88
- Domains are stored as YAML files in the configuration directory. Example domain:
108
+ **Sessions** are Claude conversation sessions identified by a UUID. When you run `cs switch`, ContextSwitch generates (or retrieves) a session ID for that domain and launches Claude with `--resume <sessionId>`, so Claude picks up where the previous conversation left off. Session IDs are deterministic -- the same domain always maps to the same base session ID.
109
+
110
+ **MCP config** at `~/.config/claude/mcp/config.json` is updated on each switch to reflect the MCP servers defined in the active domain's config. This lets each domain have its own set of MCP tools.
111
+
112
+ ### Domain Config Example
113
+
114
+ Domains are YAML files in the config directory. You can edit them directly after creating a domain with `cs domain add`:
89
115
 
90
116
  ```yaml
91
117
  name: backend
92
118
  workingDirectory: ~/projects/api
93
-
94
- claudeConfig:
95
- instructions: ./CLAUDE.md
96
- memory:
97
- - ./context/api-overview.md
119
+ metadata:
120
+ description: Backend API project
98
121
 
99
122
  mcpServers:
100
123
  filesystem:
@@ -107,79 +130,6 @@ env:
107
130
  NODE_ENV: development
108
131
  ```
109
132
 
110
- ## Project Structure
111
-
112
- ```
113
- contextswitch/
114
- ├── src/
115
- │ ├── cli.ts # Main CLI entry point
116
- │ ├── commands/ # Command implementations
117
- │ │ ├── switch.ts # Domain switching logic
118
- │ │ ├── reset.ts # Session reset
119
- │ │ └── archive.ts # Session archiving
120
- │ └── core/ # Core modules
121
- │ ├── domain.ts # Domain schemas (Zod)
122
- │ ├── config.ts # Configuration management
123
- │ ├── session.ts # Session ID generation
124
- │ ├── process.ts # Process management
125
- │ └── paths.ts # Cross-platform paths
126
- ├── domains/ # Domain templates
127
- ├── package.json
128
- ├── tsconfig.json
129
- └── README.md
130
- ```
131
-
132
- ## Configuration Files
133
-
134
- - **Domains**: `~/Library/Application Support/contextswitch/domains/*.yml` (macOS)
135
- - **State**: `~/Library/Application Support/contextswitch/state.json`
136
- - **Archives**: `~/Library/Application Support/contextswitch/archives/`
137
-
138
- ## Development
139
-
140
- ```bash
141
- # Run TypeScript compiler check
142
- npx tsc --noEmit
143
-
144
- # Run in development mode
145
- npm run dev
146
-
147
- # Build for production
148
- npm run build:node
149
-
150
- # Run tests (when implemented)
151
- npm test
152
- ```
153
-
154
- ## Technical Details
155
-
156
- - Built with TypeScript and Commander.js
157
- - Uses Zod for runtime validation
158
- - Deterministic session IDs via UUID v5
159
- - Atomic file operations for data safety
160
- - Cross-platform process management
161
-
162
- ## Status
163
-
164
- **Current Version**: 0.1.0 (MVP)
165
-
166
- ### Implemented
167
- - ✅ Core domain management
168
- - ✅ Session persistence with `--resume`
169
- - ✅ Process management (kill/spawn Claude)
170
- - ✅ State tracking and atomic writes
171
- - ✅ Cross-platform path resolution
172
- - ✅ Basic CLI commands
173
- - ✅ Archive functionality
174
-
175
- ### Not Yet Implemented
176
- - ⏳ Git sync for team sharing
177
- - ⏳ Token usage estimation
178
- - ⏳ Advanced diagnostics
179
- - ⏳ Binary compilation with Bun
180
- - ⏳ Automated tests
181
- - ⏳ Shell integration scripts
182
-
183
133
  ## License
184
134
 
185
- MIT
135
+ MIT
@@ -0,0 +1,10 @@
1
+ import {
2
+ archiveCommand,
3
+ listArchives
4
+ } from "./chunk-GHF4FLJV.js";
5
+ import "./chunk-YMFZWGZO.js";
6
+ import "./chunk-A7YXSI66.js";
7
+ export {
8
+ archiveCommand,
9
+ listArchives
10
+ };
@@ -37,16 +37,17 @@ var ProcessManager = class {
37
37
  const lines = output.split("\n");
38
38
  const processes = [];
39
39
  for (const line of lines) {
40
- if (line.includes("claude") && !line.includes("cs switch")) {
41
- const parts = line.split(/\s+/);
42
- if (parts.length >= 11 && parts[1]) {
43
- const pid = parseInt(parts[1], 10);
44
- const command = parts[10] || "";
45
- const args = parts.slice(11);
46
- if (command && (command.includes("claude") || args.some((arg) => arg && arg.includes("claude")))) {
47
- processes.push({ pid, command, args });
48
- }
49
- }
40
+ if (!line.trim()) continue;
41
+ const parts = line.split(/\s+/);
42
+ if (parts.length < 11 || !parts[1]) continue;
43
+ const pid = parseInt(parts[1], 10);
44
+ const command = parts[10] || "";
45
+ const args = parts.slice(11);
46
+ if (line.includes("cs switch")) continue;
47
+ const basename = command.split("/").pop() || "";
48
+ const isClaudeBinary = basename === "claude" || basename === "claude.exe";
49
+ if (isClaudeBinary) {
50
+ processes.push({ pid, command, args });
50
51
  }
51
52
  }
52
53
  return processes;
@@ -163,7 +164,7 @@ var ProcessManager = class {
163
164
  const cwd = options?.cwd || process.cwd();
164
165
  const gitBash = this.findGitBash();
165
166
  if (gitBash) {
166
- const claudeCommand = `cd "${cwd.replace(/\\/g, "/")}" && ${claudeCmd} ${args.join(" ")}`;
167
+ const claudeCommand = `cd "${String(cwd).replace(/\\/g, "/")}" && ${claudeCmd} ${args.join(" ")}`;
167
168
  const cmdProcess = spawn("cmd.exe", ["/c", "start", "", gitBash, "-c", claudeCommand], {
168
169
  detached: true,
169
170
  stdio: "ignore",
@@ -193,7 +194,7 @@ var ProcessManager = class {
193
194
  /**
194
195
  * Spawn a shell script in a new terminal window
195
196
  */
196
- spawnTerminalScript(script, cwd) {
197
+ spawnTerminalScript(script, _cwd) {
197
198
  const scriptFile = join(paths.baseDir, "launch.sh");
198
199
  writeFileSync(scriptFile, script, { mode: 493 });
199
200
  if (this.platform === "darwin") {
@@ -1,16 +1,9 @@
1
1
  import {
2
2
  configManager
3
- } from "./chunk-XGE4JP55.js";
4
- import {
5
- init_session,
6
- session_exports
7
- } from "./chunk-UKMZ4CUZ.js";
3
+ } from "./chunk-YMFZWGZO.js";
8
4
  import {
9
5
  paths
10
6
  } from "./chunk-A7YXSI66.js";
11
- import {
12
- __toCommonJS
13
- } from "./chunk-PNKVD2UK.js";
14
7
 
15
8
  // src/commands/archive.ts
16
9
  import picocolors from "picocolors";
@@ -63,7 +56,8 @@ async function archiveCommand(domainName) {
63
56
  console.log(pc.green(`\u2705 Session archived successfully`));
64
57
  console.log(pc.gray(`Archive: ${archivePath}`));
65
58
  console.log(pc.gray(`Size: ${sizeKB} KB`));
66
- console.log(pc.gray(`Session age: ${(init_session(), __toCommonJS(session_exports)).SessionManager.getSessionAge(session.started)}`));
59
+ const { SessionManager } = await import("./session-IWXAKW6Z.js");
60
+ console.log(pc.gray(`Session age: ${SessionManager.getSessionAge(session.started)}`));
67
61
  } catch (error) {
68
62
  console.error(pc.red(`\u274C Failed to archive session: ${error}`));
69
63
  process.exit(1);
@@ -0,0 +1,100 @@
1
+ // src/core/session.ts
2
+ import { v5 as uuidv5 } from "uuid";
3
+ import { createHash } from "crypto";
4
+ var SessionManager = class {
5
+ // Namespace UUID for ContextSwitch sessions
6
+ static NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
7
+ /**
8
+ * Generate a deterministic session ID for a domain
9
+ * Uses UUID v5 to ensure same domain always gets same base session ID
10
+ */
11
+ static generateSessionId(domainName, reset = false) {
12
+ const seed = reset ? `${domainName}:${Date.now()}` : domainName;
13
+ return uuidv5(seed, this.NAMESPACE);
14
+ }
15
+ /**
16
+ * Generate a session hash for verification
17
+ * Useful for checking if a session is still valid
18
+ */
19
+ static generateSessionHash(sessionId, domainName) {
20
+ const hash = createHash("sha256");
21
+ hash.update(`${sessionId}:${domainName}`);
22
+ return hash.digest("hex").substring(0, 8);
23
+ }
24
+ /**
25
+ * Create a session record
26
+ */
27
+ static createSessionRecord(domainName, sessionId, processId) {
28
+ return {
29
+ domain: domainName,
30
+ sessionId,
31
+ started: (/* @__PURE__ */ new Date()).toISOString(),
32
+ lastActive: (/* @__PURE__ */ new Date()).toISOString(),
33
+ processId
34
+ };
35
+ }
36
+ /**
37
+ * Check if a session is likely expired based on age
38
+ * Claude sessions typically last 1-2 weeks
39
+ */
40
+ static isSessionLikelyExpired(startedAt) {
41
+ const started = new Date(startedAt);
42
+ const now = /* @__PURE__ */ new Date();
43
+ const daysSinceStart = (now.getTime() - started.getTime()) / (1e3 * 60 * 60 * 24);
44
+ return daysSinceStart > 7;
45
+ }
46
+ /**
47
+ * Get session age in human-readable format
48
+ */
49
+ static getSessionAge(startedAt) {
50
+ const started = new Date(startedAt);
51
+ const now = /* @__PURE__ */ new Date();
52
+ const diffMs = now.getTime() - started.getTime();
53
+ const days = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
54
+ const hours = Math.floor(diffMs % (1e3 * 60 * 60 * 24) / (1e3 * 60 * 60));
55
+ if (days > 0) {
56
+ return `${days} day${days === 1 ? "" : "s"}, ${hours} hour${hours === 1 ? "" : "s"}`;
57
+ }
58
+ if (hours > 0) {
59
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
60
+ }
61
+ const minutes = Math.floor(diffMs / (1e3 * 60));
62
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
63
+ }
64
+ /**
65
+ * Get session risk level based on age
66
+ */
67
+ static getSessionRisk(startedAt) {
68
+ const started = new Date(startedAt);
69
+ const now = /* @__PURE__ */ new Date();
70
+ const daysSinceStart = (now.getTime() - started.getTime()) / (1e3 * 60 * 60 * 24);
71
+ if (daysSinceStart < 3) return "low";
72
+ if (daysSinceStart < 5) return "medium";
73
+ if (daysSinceStart < 7) return "high";
74
+ return "critical";
75
+ }
76
+ /**
77
+ * Format session info for display
78
+ */
79
+ static formatSessionInfo(session) {
80
+ const age = this.getSessionAge(session.started);
81
+ const risk = this.getSessionRisk(session.started);
82
+ const riskColors = {
83
+ low: "\u{1F7E2}",
84
+ medium: "\u{1F7E1}",
85
+ high: "\u{1F7E0}",
86
+ critical: "\u{1F534}"
87
+ };
88
+ return [
89
+ `Domain: ${session.domain}`,
90
+ `Session ID: ${session.sessionId.substring(0, 8)}...`,
91
+ `Age: ${age}`,
92
+ `Risk: ${riskColors[risk]} ${risk}`,
93
+ risk === "high" || risk === "critical" ? "\u26A0\uFE0F Consider resetting soon" : ""
94
+ ].filter(Boolean).join("\n");
95
+ }
96
+ };
97
+
98
+ export {
99
+ SessionManager
100
+ };
@@ -94,9 +94,6 @@ var ConfigManager = class {
94
94
  * Load global configuration
95
95
  */
96
96
  loadGlobalConfig() {
97
- if (this.globalConfig) {
98
- return this.globalConfig;
99
- }
100
97
  const configPath = paths.globalConfigFile;
101
98
  if (!existsSync(configPath)) {
102
99
  this.globalConfig = {
@@ -122,17 +119,16 @@ var ConfigManager = class {
122
119
  */
123
120
  saveGlobalConfig(config) {
124
121
  const configPath = paths.globalConfigFile;
122
+ const tempPath = `${configPath}.tmp`;
125
123
  const content = stringify(config, { indent: 2 });
126
- writeFileSync(configPath, content, "utf-8");
124
+ writeFileSync(tempPath, content, "utf-8");
125
+ renameSync(tempPath, configPath);
127
126
  this.globalConfig = config;
128
127
  }
129
128
  /**
130
129
  * Load state file
131
130
  */
132
131
  loadState() {
133
- if (this.state) {
134
- return this.state;
135
- }
136
132
  const statePath = paths.stateFile;
137
133
  if (!existsSync(statePath)) {
138
134
  this.state = createInitialState();
@@ -196,8 +192,10 @@ var ConfigManager = class {
196
192
  */
197
193
  saveDomain(domain) {
198
194
  const domainPath = paths.domainConfigFile(domain.name);
195
+ const tempPath = `${domainPath}.tmp`;
199
196
  const content = stringify(domain, { indent: 2 });
200
- writeFileSync(domainPath, content, "utf-8");
197
+ writeFileSync(tempPath, content, "utf-8");
198
+ renameSync(tempPath, domainPath);
201
199
  this.domainCache.set(domain.name, domain);
202
200
  }
203
201
  /**
package/dist/cli.js CHANGED
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  configManager
4
- } from "./chunk-XGE4JP55.js";
4
+ } from "./chunk-YMFZWGZO.js";
5
5
  import {
6
6
  paths
7
7
  } from "./chunk-A7YXSI66.js";
8
- import "./chunk-PNKVD2UK.js";
9
8
 
10
9
  // src/cli.ts
11
10
  import { Command } from "commander";
@@ -21,7 +20,7 @@ function getVersion() {
21
20
  const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
22
21
  return packageJson.version;
23
22
  } catch {
24
- return "0.1.3";
23
+ return "0.1.5";
25
24
  }
26
25
  }
27
26
  function processSessionFallbackMarker() {
@@ -34,10 +33,11 @@ function processSessionFallbackMarker() {
34
33
  const marker = JSON.parse(content);
35
34
  if (marker.domain && marker.sessionId) {
36
35
  const state = configManager.loadState();
37
- if (state.sessions?.[marker.domain]) {
38
- state.sessions[marker.domain].sessionId = marker.sessionId;
39
- state.sessions[marker.domain].started = (/* @__PURE__ */ new Date()).toISOString();
40
- state.sessions[marker.domain].lastActive = (/* @__PURE__ */ new Date()).toISOString();
36
+ const session = state.sessions?.[marker.domain];
37
+ if (session) {
38
+ session.sessionId = marker.sessionId;
39
+ session.started = (/* @__PURE__ */ new Date()).toISOString();
40
+ session.lastActive = (/* @__PURE__ */ new Date()).toISOString();
41
41
  state.activeSession = marker.sessionId;
42
42
  configManager.saveState(state);
43
43
  console.log(pc.yellow(`Note: Previous session for '${marker.domain}' could not be resumed.`));
@@ -54,7 +54,15 @@ function processSessionFallbackMarker() {
54
54
  }
55
55
  }
56
56
  var program = new Command();
57
- program.name("cs").description("Domain-based context management for Claude Code CLI - Each domain represents a project with its own working directory and configuration").version(getVersion()).hook("preAction", () => {
57
+ program.name("cs").description("Domain-based context management for Claude Code CLI - Each domain represents a project with its own working directory and configuration").version(getVersion()).addHelpText("after", `
58
+ Quick Start:
59
+ cs init Initialize ContextSwitch
60
+ cs domain add myproject -w /path/to/project Create a domain with a working directory
61
+ cs switch myproject Switch to the domain (launches Claude)
62
+ cs list See all your domains
63
+
64
+ The --working-dir (-w) flag sets where Claude will run. If omitted, the current directory is used.
65
+ Run "cs domain add --help" for more details.`).hook("preAction", () => {
58
66
  paths.ensureDirectories();
59
67
  processSessionFallbackMarker();
60
68
  });
@@ -130,7 +138,7 @@ program.command("status").description("Show current domain and session status").
130
138
  if (session) {
131
139
  console.log(`
132
140
  ${pc.cyan("Session Information:")}`);
133
- const { SessionManager } = await import("./session-YAMF4YD7.js");
141
+ const { SessionManager } = await import("./session-IWXAKW6Z.js");
134
142
  console.log(SessionManager.formatSessionInfo(session));
135
143
  } else {
136
144
  console.log(pc.gray("\nNo active session for this domain."));
@@ -141,15 +149,15 @@ ${pc.cyan("Session Information:")}`);
141
149
  }
142
150
  });
143
151
  program.command("switch <domain>").description("Switch to a different domain").option("-f, --force", "Force new session even if one exists").action(async (domain, options) => {
144
- const { switchCommand } = await import("./switch-MWKYEYHE.js");
152
+ const { switchCommand } = await import("./switch-RZCXBTPC.js");
145
153
  await switchCommand(domain, options);
146
154
  });
147
155
  program.command("reset <domain>").description("Reset a domain session (archives current session)").option("--skip-archive", "Skip archiving the current session").action(async (domain, options) => {
148
- const { resetCommand } = await import("./reset-3DSXZKRH.js");
156
+ const { resetCommand } = await import("./reset-GZKUYPZR.js");
149
157
  await resetCommand(domain, options);
150
158
  });
151
159
  program.command("archive <domain>").description("Archive the current session for a domain").action(async (domain) => {
152
- const { archiveCommand } = await import("./archive-3IGWZBSO.js");
160
+ const { archiveCommand } = await import("./archive-64CFJ3P5.js");
153
161
  await archiveCommand(domain);
154
162
  });
155
163
  var domainCmd = program.command("domain").description("Manage domains");
@@ -208,8 +216,9 @@ domainCmd.command("remove <name>").alias("rm").description("Remove a domain from
208
216
  }
209
217
  });
210
218
  program.command("doctor").description("Check system configuration and diagnose issues").action(async () => {
211
- const { processManager } = await import("./process-WBBCEFGG.js");
219
+ const { processManager } = await import("./process-E35QFSO6.js");
212
220
  console.log(pc.cyan("\u{1FA7A} Running diagnostics...\n"));
221
+ const failures = [];
213
222
  const claudeInstalled = processManager.verifyClaudeInstalled();
214
223
  if (claudeInstalled) {
215
224
  console.log(pc.green("\u2713 Claude CLI is installed"));
@@ -219,6 +228,7 @@ program.command("doctor").description("Check system configuration and diagnose i
219
228
  console.log(pc.gray(" npm install -g @anthropic-ai/claude-code\n"));
220
229
  console.log(pc.cyan(" Or see the official docs:"));
221
230
  console.log(pc.gray(" https://docs.anthropic.com/en/docs/claude-code\n"));
231
+ failures.push("Claude Code CLI not found");
222
232
  }
223
233
  console.log(pc.green(`\u2713 Config directory: ${paths.baseDir}`));
224
234
  const domains = configManager.listDomains();
@@ -226,7 +236,15 @@ program.command("doctor").description("Check system configuration and diagnose i
226
236
  const processes = processManager.findClaudeProcesses();
227
237
  console.log(pc.green(`\u2713 Claude processes running: ${processes.length}`));
228
238
  console.log(pc.green(`\u2713 Platform: ${paths.platform}`));
229
- console.log(pc.cyan("\n\u2728 All checks passed!"));
239
+ if (failures.length === 0) {
240
+ console.log(pc.cyan("\n\u2728 All checks passed!"));
241
+ } else {
242
+ console.log(pc.red(`
243
+ \u2717 ${failures.length} check(s) failed:`));
244
+ for (const failure of failures) {
245
+ console.log(pc.red(` - ${failure}`));
246
+ }
247
+ }
230
248
  });
231
249
  program.action(() => {
232
250
  program.outputHelp();
@@ -1,9 +1,8 @@
1
1
  import {
2
2
  ProcessManager,
3
3
  processManager
4
- } from "./chunk-56TY2J6E.js";
4
+ } from "./chunk-756VUR5T.js";
5
5
  import "./chunk-A7YXSI66.js";
6
- import "./chunk-PNKVD2UK.js";
7
6
  export {
8
7
  ProcessManager,
9
8
  processManager
@@ -1,19 +1,13 @@
1
1
  import {
2
2
  archiveCommand
3
- } from "./chunk-2LUEUGBV.js";
3
+ } from "./chunk-GHF4FLJV.js";
4
4
  import {
5
5
  configManager
6
- } from "./chunk-XGE4JP55.js";
7
- import {
8
- SessionManager,
9
- init_session
10
- } from "./chunk-UKMZ4CUZ.js";
6
+ } from "./chunk-YMFZWGZO.js";
11
7
  import "./chunk-A7YXSI66.js";
12
- import "./chunk-PNKVD2UK.js";
13
8
 
14
9
  // src/commands/reset.ts
15
10
  import picocolors from "picocolors";
16
- init_session();
17
11
  var pc = picocolors;
18
12
  async function resetCommand(domainName, options = {}) {
19
13
  try {
@@ -34,7 +28,6 @@ async function resetCommand(domainName, options = {}) {
34
28
  await archiveCommand(domainName);
35
29
  }
36
30
  }
37
- const newSessionId = SessionManager.generateSessionId(domainName, true);
38
31
  const newState = {
39
32
  ...state,
40
33
  sessions: {
@@ -48,8 +41,7 @@ async function resetCommand(domainName, options = {}) {
48
41
  }
49
42
  configManager.saveState(newState);
50
43
  console.log(pc.green(`\u2705 Domain '${domainName}' has been reset`));
51
- console.log(pc.gray(`New session will be created on next switch`));
52
- console.log(pc.gray(`Session ID: ${newSessionId.substring(0, 8)}...`));
44
+ console.log(pc.gray(`A new session will be created on next "cs switch ${domainName}"`));
53
45
  } catch (error) {
54
46
  console.error(pc.red(`\u274C Failed to reset domain: ${error}`));
55
47
  process.exit(1);
@@ -0,0 +1,6 @@
1
+ import {
2
+ SessionManager
3
+ } from "./chunk-KBKALWDX.js";
4
+ export {
5
+ SessionManager
6
+ };
@@ -1,24 +1,24 @@
1
+ import {
2
+ SessionManager
3
+ } from "./chunk-KBKALWDX.js";
1
4
  import {
2
5
  processManager
3
- } from "./chunk-56TY2J6E.js";
6
+ } from "./chunk-756VUR5T.js";
4
7
  import {
5
8
  configManager
6
- } from "./chunk-XGE4JP55.js";
7
- import {
8
- SessionManager,
9
- init_session
10
- } from "./chunk-UKMZ4CUZ.js";
9
+ } from "./chunk-YMFZWGZO.js";
11
10
  import {
12
11
  paths
13
12
  } from "./chunk-A7YXSI66.js";
14
- import "./chunk-PNKVD2UK.js";
15
13
 
16
14
  // src/commands/switch.ts
17
15
  import picocolors from "picocolors";
18
- init_session();
19
- import { existsSync, writeFileSync, readFileSync, unlinkSync, copyFileSync, mkdirSync, readdirSync } from "fs";
16
+ import { existsSync, writeFileSync, readFileSync, unlinkSync, copyFileSync, mkdirSync, readdirSync, statSync } from "fs";
20
17
  import { join, dirname, basename } from "path";
21
18
  var pc = picocolors;
19
+ function shellEscapeArg(arg) {
20
+ return "'" + arg.replace(/'/g, "'\\''") + "'";
21
+ }
22
22
  async function switchCommand(domainName, options = {}) {
23
23
  try {
24
24
  processSessionFallback();
@@ -104,8 +104,20 @@ async function updateMCPConfig(domain) {
104
104
  if (!existsSync(mcpDir)) {
105
105
  mkdirSync(mcpDir, { recursive: true });
106
106
  }
107
+ let existingConfig = {};
108
+ if (existsSync(mcpConfigPath)) {
109
+ try {
110
+ existingConfig = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
111
+ } catch {
112
+ existingConfig = {};
113
+ }
114
+ }
107
115
  const mcpConfig = {
108
- servers: domain.mcpServers || {}
116
+ ...existingConfig,
117
+ servers: {
118
+ ...existingConfig.servers || {},
119
+ ...domain.mcpServers || {}
120
+ }
109
121
  };
110
122
  writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
111
123
  console.log(pc.gray("Updated MCP configuration"));
@@ -119,10 +131,25 @@ async function handleMemoryFiles(domain) {
119
131
  if (!existsSync(memoryDir)) {
120
132
  mkdirSync(memoryDir, { recursive: true });
121
133
  }
134
+ const incomingFileNames = /* @__PURE__ */ new Set();
135
+ for (const memoryPath of domain.claudeConfig.memory) {
136
+ const expandedPath = paths.expandPath(memoryPath);
137
+ incomingFileNames.add(basename(expandedPath));
138
+ }
122
139
  if (existsSync(memoryDir)) {
123
- const files = readdirSync(memoryDir);
124
- for (const file of files) {
125
- unlinkSync(join(memoryDir, file));
140
+ const entries = readdirSync(memoryDir);
141
+ for (const entry of entries) {
142
+ const entryPath = join(memoryDir, entry);
143
+ try {
144
+ const stat = statSync(entryPath);
145
+ if (stat.isDirectory()) {
146
+ continue;
147
+ }
148
+ if (incomingFileNames.has(entry)) {
149
+ unlinkSync(entryPath);
150
+ }
151
+ } catch {
152
+ }
126
153
  }
127
154
  }
128
155
  for (const memoryPath of domain.claudeConfig.memory) {
@@ -145,15 +172,24 @@ function spawnClaudeWithFallback(domain, resumeSessionId, fallbackSessionId) {
145
172
  const cwd = domain.workingDirectory;
146
173
  const markerFile = getSessionFallbackPath();
147
174
  const domainName = domain.name;
148
- const envLines = domain.env ? Object.entries(domain.env).map(([k, v]) => `export ${k}="${v}"`).join("\n") : "";
175
+ const escapeShellValue = (val) => {
176
+ return String(val).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$");
177
+ };
178
+ const envLines = domain.env ? Object.entries(domain.env).map(([k, v]) => `export ${k}="${escapeShellValue(v)}"`).join("\n") : "";
149
179
  const markerJson = JSON.stringify({ domain: domainName, sessionId: fallbackSessionId });
180
+ const markerJsonEscaped = markerJson.replace(/'/g, "'\\''");
181
+ const escapedCwd = shellEscapeArg(cwd);
182
+ const escapedClaudeCmd = shellEscapeArg(claudeCmd);
183
+ const escapedResumeId = shellEscapeArg(resumeSessionId);
184
+ const escapedFallbackId = shellEscapeArg(fallbackSessionId);
185
+ const escapedMarkerFile = shellEscapeArg(markerFile);
150
186
  const script = [
151
187
  "#!/bin/bash",
152
- `cd "${cwd}"`,
188
+ `cd ${escapedCwd}`,
153
189
  envLines,
154
190
  "",
155
191
  "# Try to resume the existing session",
156
- `"${claudeCmd}" --resume "${resumeSessionId}"`,
192
+ `${escapedClaudeCmd} --resume ${escapedResumeId}`,
157
193
  "RESUME_EXIT=$?",
158
194
  "",
159
195
  "# If resume failed (non-zero exit), fall back to a new session",
@@ -163,9 +199,9 @@ function spawnClaudeWithFallback(domain, resumeSessionId, fallbackSessionId) {
163
199
  ' echo ""',
164
200
  "",
165
201
  " # Write marker so cs can update state with the new session ID",
166
- ` echo '${markerJson}' > "${markerFile}"`,
202
+ ` echo '${markerJsonEscaped}' > ${escapedMarkerFile}`,
167
203
  "",
168
- ` "${claudeCmd}" --session-id "${fallbackSessionId}"`,
204
+ ` ${escapedClaudeCmd} --session-id ${escapedFallbackId}`,
169
205
  "fi"
170
206
  ].join("\n");
171
207
  return processManager.spawnTerminalScript(script, cwd);
@@ -183,10 +219,11 @@ function processSessionFallback() {
183
219
  const marker = JSON.parse(content);
184
220
  if (marker.domain && marker.sessionId) {
185
221
  const state = configManager.loadState();
186
- if (state.sessions?.[marker.domain]) {
187
- state.sessions[marker.domain].sessionId = marker.sessionId;
188
- state.sessions[marker.domain].started = (/* @__PURE__ */ new Date()).toISOString();
189
- state.sessions[marker.domain].lastActive = (/* @__PURE__ */ new Date()).toISOString();
222
+ const session = state.sessions?.[marker.domain];
223
+ if (session) {
224
+ session.sessionId = marker.sessionId;
225
+ session.started = (/* @__PURE__ */ new Date()).toISOString();
226
+ session.lastActive = (/* @__PURE__ */ new Date()).toISOString();
190
227
  state.activeSession = marker.sessionId;
191
228
  configManager.saveState(state);
192
229
  console.log(pc.yellow(`Note: Previous session for '${marker.domain}' could not be resumed.`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contextswitch",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Domain-based context management for Claude Code CLI",
5
5
  "main": "dist/cli.js",
6
6
  "type": "module",
@@ -1,12 +0,0 @@
1
- import {
2
- archiveCommand,
3
- listArchives
4
- } from "./chunk-2LUEUGBV.js";
5
- import "./chunk-XGE4JP55.js";
6
- import "./chunk-UKMZ4CUZ.js";
7
- import "./chunk-A7YXSI66.js";
8
- import "./chunk-PNKVD2UK.js";
9
- export {
10
- archiveCommand,
11
- listArchives
12
- };
@@ -1,26 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __esm = (fn, res) => function __init() {
6
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
- };
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
-
22
- export {
23
- __esm,
24
- __export,
25
- __toCommonJS
26
- };
@@ -1,116 +0,0 @@
1
- import {
2
- __esm,
3
- __export
4
- } from "./chunk-PNKVD2UK.js";
5
-
6
- // src/core/session.ts
7
- var session_exports = {};
8
- __export(session_exports, {
9
- SessionManager: () => SessionManager
10
- });
11
- import { v5 as uuidv5 } from "uuid";
12
- import { createHash } from "crypto";
13
- var SessionManager;
14
- var init_session = __esm({
15
- "src/core/session.ts"() {
16
- SessionManager = class {
17
- // Namespace UUID for ContextSwitch sessions
18
- static NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
19
- /**
20
- * Generate a deterministic session ID for a domain
21
- * Uses UUID v5 to ensure same domain always gets same base session ID
22
- */
23
- static generateSessionId(domainName, reset = false) {
24
- const seed = reset ? `${domainName}:${Date.now()}` : domainName;
25
- return uuidv5(seed, this.NAMESPACE);
26
- }
27
- /**
28
- * Generate a session hash for verification
29
- * Useful for checking if a session is still valid
30
- */
31
- static generateSessionHash(sessionId, domainName) {
32
- const hash = createHash("sha256");
33
- hash.update(`${sessionId}:${domainName}`);
34
- return hash.digest("hex").substring(0, 8);
35
- }
36
- /**
37
- * Create a session record
38
- */
39
- static createSessionRecord(domainName, sessionId, processId) {
40
- return {
41
- domain: domainName,
42
- sessionId,
43
- started: (/* @__PURE__ */ new Date()).toISOString(),
44
- lastActive: (/* @__PURE__ */ new Date()).toISOString(),
45
- processId
46
- };
47
- }
48
- /**
49
- * Check if a session is likely expired based on age
50
- * Claude sessions typically last 1-2 weeks
51
- */
52
- static isSessionLikelyExpired(startedAt) {
53
- const started = new Date(startedAt);
54
- const now = /* @__PURE__ */ new Date();
55
- const daysSinceStart = (now.getTime() - started.getTime()) / (1e3 * 60 * 60 * 24);
56
- return daysSinceStart > 7;
57
- }
58
- /**
59
- * Get session age in human-readable format
60
- */
61
- static getSessionAge(startedAt) {
62
- const started = new Date(startedAt);
63
- const now = /* @__PURE__ */ new Date();
64
- const diffMs = now.getTime() - started.getTime();
65
- const days = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
66
- const hours = Math.floor(diffMs % (1e3 * 60 * 60 * 24) / (1e3 * 60 * 60));
67
- if (days > 0) {
68
- return `${days} day${days === 1 ? "" : "s"}, ${hours} hour${hours === 1 ? "" : "s"}`;
69
- }
70
- if (hours > 0) {
71
- return `${hours} hour${hours === 1 ? "" : "s"}`;
72
- }
73
- const minutes = Math.floor(diffMs / (1e3 * 60));
74
- return `${minutes} minute${minutes === 1 ? "" : "s"}`;
75
- }
76
- /**
77
- * Get session risk level based on age
78
- */
79
- static getSessionRisk(startedAt) {
80
- const started = new Date(startedAt);
81
- const now = /* @__PURE__ */ new Date();
82
- const daysSinceStart = (now.getTime() - started.getTime()) / (1e3 * 60 * 60 * 24);
83
- if (daysSinceStart < 3) return "low";
84
- if (daysSinceStart < 5) return "medium";
85
- if (daysSinceStart < 7) return "high";
86
- return "critical";
87
- }
88
- /**
89
- * Format session info for display
90
- */
91
- static formatSessionInfo(session) {
92
- const age = this.getSessionAge(session.started);
93
- const risk = this.getSessionRisk(session.started);
94
- const riskColors = {
95
- low: "\u{1F7E2}",
96
- medium: "\u{1F7E1}",
97
- high: "\u{1F7E0}",
98
- critical: "\u{1F534}"
99
- };
100
- return [
101
- `Domain: ${session.domain}`,
102
- `Session ID: ${session.sessionId.substring(0, 8)}...`,
103
- `Age: ${age}`,
104
- `Risk: ${riskColors[risk]} ${risk}`,
105
- risk === "high" || risk === "critical" ? "\u26A0\uFE0F Consider resetting soon" : ""
106
- ].filter(Boolean).join("\n");
107
- }
108
- };
109
- }
110
- });
111
-
112
- export {
113
- SessionManager,
114
- session_exports,
115
- init_session
116
- };
@@ -1,9 +0,0 @@
1
- import {
2
- SessionManager,
3
- init_session
4
- } from "./chunk-UKMZ4CUZ.js";
5
- import "./chunk-PNKVD2UK.js";
6
- init_session();
7
- export {
8
- SessionManager
9
- };