contextswitch 0.1.1
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 +185 -0
- package/dist/archive-3IGWZBSO.js +12 -0
- package/dist/chunk-2LUEUGBV.js +110 -0
- package/dist/chunk-56TY2J6E.js +366 -0
- package/dist/chunk-A7YXSI66.js +163 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/chunk-UKMZ4CUZ.js +116 -0
- package/dist/chunk-XGE4JP55.js +303 -0
- package/dist/cli.js +239 -0
- package/dist/process-WBBCEFGG.js +10 -0
- package/dist/reset-3DSXZKRH.js +60 -0
- package/dist/session-YAMF4YD7.js +9 -0
- package/dist/switch-MWKYEYHE.js +218 -0
- package/package.json +61 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
configManager
|
|
4
|
+
} from "./chunk-XGE4JP55.js";
|
|
5
|
+
import {
|
|
6
|
+
paths
|
|
7
|
+
} from "./chunk-A7YXSI66.js";
|
|
8
|
+
import "./chunk-PNKVD2UK.js";
|
|
9
|
+
|
|
10
|
+
// src/cli.ts
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
import picocolors from "picocolors";
|
|
13
|
+
import { existsSync, readFileSync, unlinkSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
var pc = picocolors;
|
|
16
|
+
function getVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const packagePath = join(__dirname, "..", "package.json");
|
|
19
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
20
|
+
return packageJson.version;
|
|
21
|
+
} catch {
|
|
22
|
+
return "0.1.0";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function processSessionFallbackMarker() {
|
|
26
|
+
const markerPath = join(paths.baseDir, "session-fallback.json");
|
|
27
|
+
if (!existsSync(markerPath)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(markerPath, "utf-8");
|
|
32
|
+
const marker = JSON.parse(content);
|
|
33
|
+
if (marker.domain && marker.sessionId) {
|
|
34
|
+
const state = configManager.loadState();
|
|
35
|
+
if (state.sessions?.[marker.domain]) {
|
|
36
|
+
state.sessions[marker.domain].sessionId = marker.sessionId;
|
|
37
|
+
state.sessions[marker.domain].started = (/* @__PURE__ */ new Date()).toISOString();
|
|
38
|
+
state.sessions[marker.domain].lastActive = (/* @__PURE__ */ new Date()).toISOString();
|
|
39
|
+
state.activeSession = marker.sessionId;
|
|
40
|
+
configManager.saveState(state);
|
|
41
|
+
console.log(pc.yellow(`Note: Previous session for '${marker.domain}' could not be resumed.`));
|
|
42
|
+
console.log(pc.yellow(`A new session (${marker.sessionId.substring(0, 8)}...) was started automatically.
|
|
43
|
+
`));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
unlinkSync(markerPath);
|
|
47
|
+
} catch {
|
|
48
|
+
try {
|
|
49
|
+
unlinkSync(markerPath);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
var program = new Command();
|
|
55
|
+
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", () => {
|
|
56
|
+
paths.ensureDirectories();
|
|
57
|
+
processSessionFallbackMarker();
|
|
58
|
+
});
|
|
59
|
+
program.command("init").description("Initialize ContextSwitch in the current directory").action(async () => {
|
|
60
|
+
try {
|
|
61
|
+
console.log(pc.cyan("\u{1F680} Initializing ContextSwitch..."));
|
|
62
|
+
paths.ensureDirectories();
|
|
63
|
+
if (!configManager.domainExists("default")) {
|
|
64
|
+
console.log(pc.gray("Creating default domain..."));
|
|
65
|
+
configManager.createDomain("default", {
|
|
66
|
+
workingDirectory: process.cwd()
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
configManager.loadGlobalConfig();
|
|
70
|
+
console.log(pc.gray(`Configuration directory: ${paths.baseDir}`));
|
|
71
|
+
console.log(pc.green("\u2705 ContextSwitch initialized successfully!"));
|
|
72
|
+
console.log(pc.gray("\nNext steps:"));
|
|
73
|
+
console.log(pc.gray(" \u2022 Create a new domain: cs domain add <name> --working-dir <path>"));
|
|
74
|
+
console.log(pc.gray(" Example: cs domain add backend --working-dir ~/projects/api"));
|
|
75
|
+
console.log(pc.gray(" \u2022 Switch to a domain: cs switch <domain>"));
|
|
76
|
+
console.log(pc.gray(" \u2022 List domains: cs list"));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(pc.red(`\u274C Initialization failed: ${error}`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
program.command("list").alias("ls").description("List all available domains").action(async () => {
|
|
83
|
+
try {
|
|
84
|
+
const domains = configManager.listDomains();
|
|
85
|
+
const state = configManager.loadState();
|
|
86
|
+
if (domains.length === 0) {
|
|
87
|
+
console.log(pc.yellow('No domains found. Run "cs init" to get started.'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(pc.cyan("\u{1F4CB} Available domains:\n"));
|
|
91
|
+
for (const domainName of domains) {
|
|
92
|
+
const isActive = state.activeDomain === domainName;
|
|
93
|
+
const domain = configManager.loadDomain(domainName);
|
|
94
|
+
if (isActive) {
|
|
95
|
+
console.log(pc.green(`\u25CF ${domainName} (active)`));
|
|
96
|
+
} else {
|
|
97
|
+
console.log(` ${domainName}`);
|
|
98
|
+
}
|
|
99
|
+
if (domain.metadata?.description) {
|
|
100
|
+
console.log(pc.gray(` ${domain.metadata.description}`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.log(pc.gray(`
|
|
104
|
+
Total: ${domains.length} domain(s)`));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(pc.red(`\u274C Failed to list domains: ${error}`));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
program.command("status").description("Show current domain and session status").argument("[domain]", "Domain to check status for (default: current)").action(async (domain) => {
|
|
111
|
+
try {
|
|
112
|
+
const state = configManager.loadState();
|
|
113
|
+
if (!domain) {
|
|
114
|
+
domain = state.activeDomain || void 0;
|
|
115
|
+
}
|
|
116
|
+
if (!domain) {
|
|
117
|
+
console.log(pc.yellow('No active domain. Use "cs switch <domain>" to activate one.'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const domainConfig = configManager.loadDomain(domain);
|
|
121
|
+
const session = state.sessions?.[domain];
|
|
122
|
+
console.log(pc.cyan(`\u{1F4CA} Domain Status: ${domain}
|
|
123
|
+
`));
|
|
124
|
+
console.log(`Working Directory: ${domainConfig.workingDirectory}`);
|
|
125
|
+
if (domainConfig.mcpServers && Object.keys(domainConfig.mcpServers).length > 0) {
|
|
126
|
+
console.log(`MCP Servers: ${Object.keys(domainConfig.mcpServers).join(", ")}`);
|
|
127
|
+
}
|
|
128
|
+
if (session) {
|
|
129
|
+
console.log(`
|
|
130
|
+
${pc.cyan("Session Information:")}`);
|
|
131
|
+
const { SessionManager } = await import("./session-YAMF4YD7.js");
|
|
132
|
+
console.log(SessionManager.formatSessionInfo(session));
|
|
133
|
+
} else {
|
|
134
|
+
console.log(pc.gray("\nNo active session for this domain."));
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(pc.red(`\u274C Failed to get status: ${error}`));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
program.command("switch <domain>").description("Switch to a different domain").option("-f, --force", "Force new session even if one exists").action(async (domain, options) => {
|
|
142
|
+
const { switchCommand } = await import("./switch-MWKYEYHE.js");
|
|
143
|
+
await switchCommand(domain, options);
|
|
144
|
+
});
|
|
145
|
+
program.command("reset <domain>").description("Reset a domain session (archives current session)").option("--skip-archive", "Skip archiving the current session").action(async (domain, options) => {
|
|
146
|
+
const { resetCommand } = await import("./reset-3DSXZKRH.js");
|
|
147
|
+
await resetCommand(domain, options);
|
|
148
|
+
});
|
|
149
|
+
program.command("archive <domain>").description("Archive the current session for a domain").action(async (domain) => {
|
|
150
|
+
const { archiveCommand } = await import("./archive-3IGWZBSO.js");
|
|
151
|
+
await archiveCommand(domain);
|
|
152
|
+
});
|
|
153
|
+
var domainCmd = program.command("domain").description("Manage domains");
|
|
154
|
+
domainCmd.command("add <name>").description('Create a new domain\n\nExamples:\n cs domain add myapi --working-dir ~/projects/api\n cs domain add frontend -w /home/user/app -d "React frontend"').option("-w, --working-dir <path>", "Working directory for this domain (defaults to current directory)").option("-d, --description <text>", "Domain description").option("--extends <parent>", "Inherit from parent domain").action(async (name, options) => {
|
|
155
|
+
try {
|
|
156
|
+
if (configManager.domainExists(name)) {
|
|
157
|
+
console.error(pc.red(`\u274C Domain '${name}' already exists`));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const workingDir = options.workingDir || process.cwd();
|
|
161
|
+
console.log(pc.cyan(`\u{1F3AF} Creating domain '${name}'...`));
|
|
162
|
+
configManager.createDomain(name, {
|
|
163
|
+
workingDirectory: workingDir,
|
|
164
|
+
extends: options.extends,
|
|
165
|
+
metadata: {
|
|
166
|
+
description: options.description,
|
|
167
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
console.log(pc.green(`\u2705 Domain '${name}' created successfully!`));
|
|
171
|
+
console.log(pc.gray(`
|
|
172
|
+
Working directory: ${workingDir}`));
|
|
173
|
+
console.log(pc.gray(`Configuration file: ${paths.domainConfigFile(name)}`));
|
|
174
|
+
console.log(pc.gray(`
|
|
175
|
+
Next steps:`));
|
|
176
|
+
console.log(pc.gray(` \u2022 Switch to domain: cs switch ${name}`));
|
|
177
|
+
console.log(pc.gray(` \u2022 Edit configuration (optional): ${paths.domainConfigFile(name)}`));
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(pc.red(`\u274C Failed to create domain: ${error}`));
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
domainCmd.command("remove <name>").alias("rm").description("Remove a domain from ContextSwitch (does not affect working directory)").action(async (name) => {
|
|
184
|
+
try {
|
|
185
|
+
if (!configManager.domainExists(name)) {
|
|
186
|
+
console.error(pc.red(`\u274C Domain '${name}' not found`));
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
const state = configManager.loadState();
|
|
190
|
+
if (state.activeDomain === name) {
|
|
191
|
+
console.error(pc.red(`\u274C Cannot remove active domain '${name}'`));
|
|
192
|
+
console.log(pc.gray("Switch to a different domain first."));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
if (state.sessions?.[name]) {
|
|
196
|
+
delete state.sessions[name];
|
|
197
|
+
configManager.saveState(state);
|
|
198
|
+
}
|
|
199
|
+
configManager.deleteDomain(name);
|
|
200
|
+
console.log(pc.green(`\u2705 Domain '${name}' removed successfully`));
|
|
201
|
+
console.log(pc.gray("The working directory was not modified."));
|
|
202
|
+
console.log(pc.gray(`You can recreate this domain later with: cs domain add ${name}`));
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error(pc.red(`\u274C Failed to remove domain: ${error}`));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
program.command("doctor").description("Check system configuration and diagnose issues").action(async () => {
|
|
209
|
+
const { processManager } = await import("./process-WBBCEFGG.js");
|
|
210
|
+
console.log(pc.cyan("\u{1FA7A} Running diagnostics...\n"));
|
|
211
|
+
const claudeInstalled = processManager.verifyClaudeInstalled();
|
|
212
|
+
if (claudeInstalled) {
|
|
213
|
+
console.log(pc.green("\u2713 Claude CLI is installed"));
|
|
214
|
+
} else {
|
|
215
|
+
console.log(pc.red("\u2717 Claude Code CLI not found\n"));
|
|
216
|
+
console.log(pc.cyan(" Install via npm (recommended):"));
|
|
217
|
+
console.log(pc.gray(" npm install -g @anthropic-ai/claude-code\n"));
|
|
218
|
+
console.log(pc.cyan(" Or see the official docs:"));
|
|
219
|
+
console.log(pc.gray(" https://docs.anthropic.com/en/docs/claude-code\n"));
|
|
220
|
+
}
|
|
221
|
+
console.log(pc.green(`\u2713 Config directory: ${paths.baseDir}`));
|
|
222
|
+
const domains = configManager.listDomains();
|
|
223
|
+
console.log(pc.green(`\u2713 Domains configured: ${domains.length}`));
|
|
224
|
+
const processes = processManager.findClaudeProcesses();
|
|
225
|
+
console.log(pc.green(`\u2713 Claude processes running: ${processes.length}`));
|
|
226
|
+
console.log(pc.green(`\u2713 Platform: ${paths.platform}`));
|
|
227
|
+
console.log(pc.cyan("\n\u2728 All checks passed!"));
|
|
228
|
+
});
|
|
229
|
+
program.exitOverride((err) => {
|
|
230
|
+
if (err.code === "commander.helpDisplayed") {
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
if (err.code === "commander.version") {
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
237
|
+
process.exit(1);
|
|
238
|
+
});
|
|
239
|
+
program.parse();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
archiveCommand
|
|
3
|
+
} from "./chunk-2LUEUGBV.js";
|
|
4
|
+
import {
|
|
5
|
+
configManager
|
|
6
|
+
} from "./chunk-XGE4JP55.js";
|
|
7
|
+
import {
|
|
8
|
+
SessionManager,
|
|
9
|
+
init_session
|
|
10
|
+
} from "./chunk-UKMZ4CUZ.js";
|
|
11
|
+
import "./chunk-A7YXSI66.js";
|
|
12
|
+
import "./chunk-PNKVD2UK.js";
|
|
13
|
+
|
|
14
|
+
// src/commands/reset.ts
|
|
15
|
+
import picocolors from "picocolors";
|
|
16
|
+
init_session();
|
|
17
|
+
var pc = picocolors;
|
|
18
|
+
async function resetCommand(domainName, options = {}) {
|
|
19
|
+
try {
|
|
20
|
+
if (!configManager.domainExists(domainName)) {
|
|
21
|
+
throw new Error(`Domain '${domainName}' not found`);
|
|
22
|
+
}
|
|
23
|
+
const state = configManager.loadState();
|
|
24
|
+
const session = state.sessions?.[domainName];
|
|
25
|
+
if (!session) {
|
|
26
|
+
console.log(pc.yellow(`Domain '${domainName}' has no active session to reset.`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(pc.cyan(`\u{1F504} Resetting domain '${domainName}'...`));
|
|
30
|
+
if (!options.skipArchive) {
|
|
31
|
+
const globalConfig = configManager.loadGlobalConfig();
|
|
32
|
+
if (globalConfig.autoArchive) {
|
|
33
|
+
console.log(pc.gray("Archiving current session..."));
|
|
34
|
+
await archiveCommand(domainName);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const newSessionId = SessionManager.generateSessionId(domainName, true);
|
|
38
|
+
const newState = {
|
|
39
|
+
...state,
|
|
40
|
+
sessions: {
|
|
41
|
+
...state.sessions
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
delete newState.sessions[domainName];
|
|
45
|
+
if (state.activeDomain === domainName) {
|
|
46
|
+
newState.activeDomain = null;
|
|
47
|
+
newState.activeSession = null;
|
|
48
|
+
}
|
|
49
|
+
configManager.saveState(newState);
|
|
50
|
+
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)}...`));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(pc.red(`\u274C Failed to reset domain: ${error}`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
resetCommand
|
|
60
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import {
|
|
2
|
+
processManager
|
|
3
|
+
} from "./chunk-56TY2J6E.js";
|
|
4
|
+
import {
|
|
5
|
+
configManager
|
|
6
|
+
} from "./chunk-XGE4JP55.js";
|
|
7
|
+
import {
|
|
8
|
+
SessionManager,
|
|
9
|
+
init_session
|
|
10
|
+
} from "./chunk-UKMZ4CUZ.js";
|
|
11
|
+
import {
|
|
12
|
+
paths
|
|
13
|
+
} from "./chunk-A7YXSI66.js";
|
|
14
|
+
import "./chunk-PNKVD2UK.js";
|
|
15
|
+
|
|
16
|
+
// src/commands/switch.ts
|
|
17
|
+
import picocolors from "picocolors";
|
|
18
|
+
init_session();
|
|
19
|
+
import { existsSync, writeFileSync, readFileSync, unlinkSync, copyFileSync, mkdirSync, readdirSync } from "fs";
|
|
20
|
+
import { join, dirname, basename } from "path";
|
|
21
|
+
var pc = picocolors;
|
|
22
|
+
async function switchCommand(domainName, options = {}) {
|
|
23
|
+
try {
|
|
24
|
+
processSessionFallback();
|
|
25
|
+
console.log(pc.cyan(`\u{1F504} Switching to domain '${domainName}'...`));
|
|
26
|
+
if (!processManager.verifyClaudeInstalled()) {
|
|
27
|
+
printClaudeInstallHelp();
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
if (!configManager.domainExists(domainName)) {
|
|
31
|
+
throw new Error(`Domain '${domainName}' not found`);
|
|
32
|
+
}
|
|
33
|
+
const domain = configManager.loadDomain(domainName);
|
|
34
|
+
const state = configManager.loadState();
|
|
35
|
+
if (state.activeDomain === domainName && !options.force) {
|
|
36
|
+
console.log(pc.yellow(`Domain '${domainName}' is already active.`));
|
|
37
|
+
console.log(pc.gray("Use --force to restart the session."));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log(pc.gray("Terminating existing Claude sessions..."));
|
|
41
|
+
await processManager.killAllClaudeProcesses();
|
|
42
|
+
if (state.activeDomain) {
|
|
43
|
+
processManager.deletePidFile(state.activeDomain);
|
|
44
|
+
}
|
|
45
|
+
let sessionId;
|
|
46
|
+
let isNewSession = false;
|
|
47
|
+
if (state.sessions?.[domainName] && !options.force) {
|
|
48
|
+
const session = state.sessions[domainName];
|
|
49
|
+
sessionId = session.sessionId;
|
|
50
|
+
const risk = SessionManager.getSessionRisk(session.started);
|
|
51
|
+
if (risk === "high" || risk === "critical") {
|
|
52
|
+
console.log(pc.yellow(`\u26A0\uFE0F Session is ${SessionManager.getSessionAge(session.started)} old`));
|
|
53
|
+
console.log(pc.yellow('Consider using "cs reset" if you experience issues.'));
|
|
54
|
+
}
|
|
55
|
+
console.log(pc.gray(`Resuming session ${sessionId.substring(0, 8)}...`));
|
|
56
|
+
} else {
|
|
57
|
+
sessionId = SessionManager.generateSessionId(domainName, true);
|
|
58
|
+
isNewSession = true;
|
|
59
|
+
console.log(pc.gray(`Creating new session ${sessionId.substring(0, 8)}...`));
|
|
60
|
+
}
|
|
61
|
+
await updateMCPConfig(domain);
|
|
62
|
+
await handleMemoryFiles(domain);
|
|
63
|
+
console.log(pc.gray("Starting Claude with new configuration..."));
|
|
64
|
+
let pid;
|
|
65
|
+
if (!isNewSession) {
|
|
66
|
+
const fallbackSessionId = SessionManager.generateSessionId(domainName, true);
|
|
67
|
+
pid = spawnClaudeWithFallback(domain, sessionId, fallbackSessionId);
|
|
68
|
+
} else {
|
|
69
|
+
pid = processManager.spawnClaude(["--session-id", sessionId], {
|
|
70
|
+
cwd: domain.workingDirectory,
|
|
71
|
+
env: {
|
|
72
|
+
...process.env,
|
|
73
|
+
...domain.env
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const newState = {
|
|
78
|
+
...state,
|
|
79
|
+
activeDomain: domainName,
|
|
80
|
+
activeSession: sessionId,
|
|
81
|
+
lastSwitch: (/* @__PURE__ */ new Date()).toISOString(),
|
|
82
|
+
sessions: {
|
|
83
|
+
...state.sessions,
|
|
84
|
+
[domainName]: SessionManager.createSessionRecord(domainName, sessionId, pid)
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
configManager.saveState(newState);
|
|
88
|
+
if (pid) {
|
|
89
|
+
processManager.writePidFile(domainName, pid);
|
|
90
|
+
}
|
|
91
|
+
console.log(pc.green(`\u2705 Switched to domain '${domainName}'`));
|
|
92
|
+
console.log(pc.gray(`Working directory: ${domain.workingDirectory}`));
|
|
93
|
+
if (domain.mcpServers && Object.keys(domain.mcpServers).length > 0) {
|
|
94
|
+
console.log(pc.gray(`MCP servers: ${Object.keys(domain.mcpServers).join(", ")}`));
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(pc.red(`\u274C Failed to switch domain: ${error}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function updateMCPConfig(domain) {
|
|
102
|
+
const mcpConfigPath = paths.claudeMCPConfig;
|
|
103
|
+
const mcpDir = dirname(mcpConfigPath);
|
|
104
|
+
if (!existsSync(mcpDir)) {
|
|
105
|
+
mkdirSync(mcpDir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
const mcpConfig = {
|
|
108
|
+
servers: domain.mcpServers || {}
|
|
109
|
+
};
|
|
110
|
+
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
111
|
+
console.log(pc.gray("Updated MCP configuration"));
|
|
112
|
+
}
|
|
113
|
+
async function handleMemoryFiles(domain) {
|
|
114
|
+
if (!domain.claudeConfig?.memory || domain.claudeConfig.memory.length === 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const claudeDir = paths.claudeConfigDir;
|
|
118
|
+
const memoryDir = join(claudeDir, "memory");
|
|
119
|
+
if (!existsSync(memoryDir)) {
|
|
120
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
if (existsSync(memoryDir)) {
|
|
123
|
+
const files = readdirSync(memoryDir);
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
unlinkSync(join(memoryDir, file));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const memoryPath of domain.claudeConfig.memory) {
|
|
129
|
+
const expandedPath = paths.expandPath(memoryPath);
|
|
130
|
+
if (!existsSync(expandedPath)) {
|
|
131
|
+
console.log(pc.yellow(`Warning: Memory file not found: ${memoryPath}`));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const fileName = basename(expandedPath);
|
|
135
|
+
const destPath = join(memoryDir, fileName);
|
|
136
|
+
copyFileSync(expandedPath, destPath);
|
|
137
|
+
console.log(pc.gray(`Loaded memory file: ${fileName}`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function spawnClaudeWithFallback(domain, resumeSessionId, fallbackSessionId) {
|
|
141
|
+
const claudeCmd = processManager.getClaudeExecutable();
|
|
142
|
+
if (!claudeCmd) {
|
|
143
|
+
throw new Error("Claude CLI not found");
|
|
144
|
+
}
|
|
145
|
+
const cwd = domain.workingDirectory;
|
|
146
|
+
const markerFile = getSessionFallbackPath();
|
|
147
|
+
const domainName = domain.name;
|
|
148
|
+
const envLines = domain.env ? Object.entries(domain.env).map(([k, v]) => `export ${k}="${v}"`).join("\n") : "";
|
|
149
|
+
const markerJson = JSON.stringify({ domain: domainName, sessionId: fallbackSessionId });
|
|
150
|
+
const script = [
|
|
151
|
+
"#!/bin/bash",
|
|
152
|
+
`cd "${cwd}"`,
|
|
153
|
+
envLines,
|
|
154
|
+
"",
|
|
155
|
+
"# Try to resume the existing session",
|
|
156
|
+
`"${claudeCmd}" --resume "${resumeSessionId}"`,
|
|
157
|
+
"RESUME_EXIT=$?",
|
|
158
|
+
"",
|
|
159
|
+
"# If resume failed (non-zero exit), fall back to a new session",
|
|
160
|
+
"if [ $RESUME_EXIT -ne 0 ]; then",
|
|
161
|
+
' echo ""',
|
|
162
|
+
' echo "Previous session could not be resumed. Starting a fresh session..."',
|
|
163
|
+
' echo ""',
|
|
164
|
+
"",
|
|
165
|
+
" # Write marker so cs can update state with the new session ID",
|
|
166
|
+
` echo '${markerJson}' > "${markerFile}"`,
|
|
167
|
+
"",
|
|
168
|
+
` "${claudeCmd}" --session-id "${fallbackSessionId}"`,
|
|
169
|
+
"fi"
|
|
170
|
+
].join("\n");
|
|
171
|
+
return processManager.spawnTerminalScript(script, cwd);
|
|
172
|
+
}
|
|
173
|
+
function getSessionFallbackPath() {
|
|
174
|
+
return join(paths.baseDir, "session-fallback.json");
|
|
175
|
+
}
|
|
176
|
+
function processSessionFallback() {
|
|
177
|
+
const markerPath = getSessionFallbackPath();
|
|
178
|
+
if (!existsSync(markerPath)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const content = readFileSync(markerPath, "utf-8");
|
|
183
|
+
const marker = JSON.parse(content);
|
|
184
|
+
if (marker.domain && marker.sessionId) {
|
|
185
|
+
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();
|
|
190
|
+
state.activeSession = marker.sessionId;
|
|
191
|
+
configManager.saveState(state);
|
|
192
|
+
console.log(pc.yellow(`Note: Previous session for '${marker.domain}' could not be resumed.`));
|
|
193
|
+
console.log(pc.yellow(`A new session (${marker.sessionId.substring(0, 8)}...) was started automatically.
|
|
194
|
+
`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
unlinkSync(markerPath);
|
|
198
|
+
} catch {
|
|
199
|
+
try {
|
|
200
|
+
unlinkSync(markerPath);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function printClaudeInstallHelp() {
|
|
206
|
+
console.error(pc.red("\u274C Claude Code CLI not found\n"));
|
|
207
|
+
console.log(pc.white("ContextSwitch requires the Claude Code CLI to be installed.\n"));
|
|
208
|
+
console.log(pc.cyan("Install via npm (recommended):"));
|
|
209
|
+
console.log(pc.gray(" npm install -g @anthropic-ai/claude-code\n"));
|
|
210
|
+
console.log(pc.cyan("Or see the official docs:"));
|
|
211
|
+
console.log(pc.gray(" https://docs.anthropic.com/en/docs/claude-code\n"));
|
|
212
|
+
console.log(pc.white("After installing, verify it works:"));
|
|
213
|
+
console.log(pc.gray(" claude --version\n"));
|
|
214
|
+
console.log(pc.gray("Then try your command again."));
|
|
215
|
+
}
|
|
216
|
+
export {
|
|
217
|
+
switchCommand
|
|
218
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "contextswitch",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Domain-based context management for Claude Code CLI",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cs": "./dist/cli.js",
|
|
9
|
+
"contextswitch": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"package.json"
|
|
14
|
+
],
|
|
15
|
+
"preferGlobal": true,
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "bun run src/cli.ts",
|
|
18
|
+
"build": "bun build src/cli.ts --compile --outfile cs",
|
|
19
|
+
"build:node": "tsup src/cli.ts --format esm --target node18",
|
|
20
|
+
"prepublishOnly": "npm run build:node",
|
|
21
|
+
"test": "vitest",
|
|
22
|
+
"test:coverage": "vitest run --coverage",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"lint": "eslint src/**/*.ts",
|
|
25
|
+
"format": "prettier --write src/**/*.ts"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"claude",
|
|
29
|
+
"cli",
|
|
30
|
+
"context",
|
|
31
|
+
"domain",
|
|
32
|
+
"mcp",
|
|
33
|
+
"session"
|
|
34
|
+
],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^11.1.0",
|
|
39
|
+
"yaml": "^2.3.4",
|
|
40
|
+
"zod": "^3.22.4",
|
|
41
|
+
"uuid": "^9.0.1",
|
|
42
|
+
"picocolors": "^1.0.0",
|
|
43
|
+
"enquirer": "^2.4.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.10.0",
|
|
47
|
+
"@types/uuid": "^9.0.7",
|
|
48
|
+
"typescript": "^5.3.3",
|
|
49
|
+
"vitest": "^1.0.4",
|
|
50
|
+
"tsup": "^8.0.1",
|
|
51
|
+
"@vitest/coverage-v8": "^1.0.4",
|
|
52
|
+
"eslint": "^8.55.0",
|
|
53
|
+
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
|
54
|
+
"@typescript-eslint/parser": "^6.14.0",
|
|
55
|
+
"prettier": "^3.1.1"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0",
|
|
59
|
+
"bun": ">=1.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|