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 +85 -135
- package/dist/archive-64CFJ3P5.js +10 -0
- package/dist/{chunk-56TY2J6E.js → chunk-756VUR5T.js} +13 -12
- package/dist/{chunk-2LUEUGBV.js → chunk-GHF4FLJV.js} +3 -9
- package/dist/chunk-KBKALWDX.js +100 -0
- package/dist/{chunk-XGE4JP55.js → chunk-YMFZWGZO.js} +6 -8
- package/dist/cli.js +32 -14
- package/dist/{process-WBBCEFGG.js → process-E35QFSO6.js} +1 -2
- package/dist/{reset-3DSXZKRH.js → reset-GZKUYPZR.js} +3 -11
- package/dist/session-IWXAKW6Z.js +6 -0
- package/dist/{switch-MWKYEYHE.js → switch-RZCXBTPC.js} +59 -22
- package/package.json +1 -1
- package/dist/archive-3IGWZBSO.js +0 -12
- package/dist/chunk-PNKVD2UK.js +0 -26
- package/dist/chunk-UKMZ4CUZ.js +0 -116
- package/dist/session-YAMF4YD7.js +0 -9
package/README.md
CHANGED
|
@@ -1,100 +1,123 @@
|
|
|
1
1
|
# ContextSwitch
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
#
|
|
23
|
-
|
|
24
|
-
./install.sh
|
|
27
|
+
# See all domains
|
|
28
|
+
cs list
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
## Commands
|
|
28
32
|
|
|
29
|
-
###
|
|
30
|
-
```bash
|
|
31
|
-
# Install dependencies
|
|
32
|
-
npm install
|
|
33
|
+
### Core
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
echo "alias cs='node $(pwd)/dist/cli.js'" >> ~/.zshrc
|
|
39
|
-
source ~/.zshrc
|
|
42
|
+
**switch** accepts an optional flag:
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
cs
|
|
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
|
-
|
|
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
|
-
#
|
|
49
|
-
|
|
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
|
-
#
|
|
52
|
-
|
|
69
|
+
# Inherit settings from another domain
|
|
70
|
+
cs domain add staging --extends backend
|
|
71
|
+
```
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
node dist/cli.js switch backend
|
|
73
|
+
If `--working-dir` is omitted, the current directory is used.
|
|
56
74
|
|
|
57
|
-
|
|
58
|
-
node dist/cli.js status
|
|
75
|
+
**reset** options:
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
82
|
+
### Diagnostics
|
|
65
83
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
- macOS: `~/Library/Application Support/contextswitch/`
|
|
101
|
+
- Linux: `~/.config/contextswitch/`
|
|
102
|
+
- Windows: `%APPDATA%\contextswitch\`
|
|
81
103
|
|
|
82
|
-
|
|
83
|
-
- `cs --help` - Show help information
|
|
84
|
-
- `cs --version` - Show version
|
|
104
|
+
## How It Works
|
|
85
105
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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 (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,
|
|
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-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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-
|
|
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.
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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()).
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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,19 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
archiveCommand
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GHF4FLJV.js";
|
|
4
4
|
import {
|
|
5
5
|
configManager
|
|
6
|
-
} from "./chunk-
|
|
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(`
|
|
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);
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SessionManager
|
|
3
|
+
} from "./chunk-KBKALWDX.js";
|
|
1
4
|
import {
|
|
2
5
|
processManager
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-756VUR5T.js";
|
|
4
7
|
import {
|
|
5
8
|
configManager
|
|
6
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
124
|
-
for (const
|
|
125
|
-
|
|
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
|
|
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
|
|
188
|
+
`cd ${escapedCwd}`,
|
|
153
189
|
envLines,
|
|
154
190
|
"",
|
|
155
191
|
"# Try to resume the existing session",
|
|
156
|
-
|
|
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 '${
|
|
202
|
+
` echo '${markerJsonEscaped}' > ${escapedMarkerFile}`,
|
|
167
203
|
"",
|
|
168
|
-
`
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
package/dist/archive-3IGWZBSO.js
DELETED
package/dist/chunk-PNKVD2UK.js
DELETED
|
@@ -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
|
-
};
|
package/dist/chunk-UKMZ4CUZ.js
DELETED
|
@@ -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
|
-
};
|