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.
@@ -0,0 +1,163 @@
1
+ // src/core/paths.ts
2
+ import { homedir, platform } from "os";
3
+ import { join, resolve, normalize } from "path";
4
+ import { existsSync, mkdirSync } from "fs";
5
+ var Paths = class _Paths {
6
+ static _instance;
7
+ _baseDir;
8
+ _platform;
9
+ constructor() {
10
+ this._platform = platform();
11
+ this._baseDir = this.getDefaultBaseDir();
12
+ }
13
+ static get instance() {
14
+ if (!this._instance) {
15
+ this._instance = new _Paths();
16
+ }
17
+ return this._instance;
18
+ }
19
+ /**
20
+ * Get the default base directory for ContextSwitch config
21
+ */
22
+ getDefaultBaseDir() {
23
+ const home = homedir();
24
+ switch (this._platform) {
25
+ case "win32":
26
+ const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
27
+ return join(appData, "contextswitch");
28
+ case "darwin":
29
+ return join(home, "Library", "Application Support", "contextswitch");
30
+ default:
31
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(home, ".config");
32
+ return join(xdgConfig, "contextswitch");
33
+ }
34
+ }
35
+ /**
36
+ * Override base directory (useful for testing)
37
+ */
38
+ setBaseDir(dir) {
39
+ this._baseDir = resolve(dir);
40
+ }
41
+ /**
42
+ * Get base configuration directory
43
+ */
44
+ get baseDir() {
45
+ return this._baseDir;
46
+ }
47
+ /**
48
+ * Get domains directory
49
+ */
50
+ get domainsDir() {
51
+ return join(this._baseDir, "domains");
52
+ }
53
+ /**
54
+ * Get archives directory
55
+ */
56
+ get archivesDir() {
57
+ return join(this._baseDir, "archives");
58
+ }
59
+ /**
60
+ * Get state file path
61
+ */
62
+ get stateFile() {
63
+ return join(this._baseDir, "state.json");
64
+ }
65
+ /**
66
+ * Get global config file path
67
+ */
68
+ get globalConfigFile() {
69
+ return join(this._baseDir, "config.yml");
70
+ }
71
+ /**
72
+ * Get domain config file path
73
+ */
74
+ domainConfigFile(domainName) {
75
+ return join(this.domainsDir, `${domainName}.yml`);
76
+ }
77
+ /**
78
+ * Get archive file path for a domain
79
+ */
80
+ archiveFile(domainName, timestamp) {
81
+ const ts = timestamp || /* @__PURE__ */ new Date();
82
+ const dateStr = ts.toISOString().replace(/[:.]/g, "-");
83
+ return join(this.archivesDir, domainName, `${dateStr}.tar.gz`);
84
+ }
85
+ /**
86
+ * Get Claude config directory
87
+ */
88
+ get claudeConfigDir() {
89
+ const home = homedir();
90
+ switch (this._platform) {
91
+ case "win32":
92
+ const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
93
+ return join(appData, "Claude");
94
+ case "darwin":
95
+ return join(home, "Library", "Application Support", "Claude");
96
+ default:
97
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(home, ".config");
98
+ return join(xdgConfig, "claude");
99
+ }
100
+ }
101
+ /**
102
+ * Get MCP config file path for Claude
103
+ */
104
+ get claudeMCPConfig() {
105
+ return join(this.claudeConfigDir, "mcp", "config.json");
106
+ }
107
+ /**
108
+ * Expand environment variables and ~ in paths
109
+ */
110
+ expandPath(inputPath) {
111
+ if (inputPath.startsWith("~/")) {
112
+ inputPath = join(homedir(), inputPath.slice(2));
113
+ }
114
+ inputPath = inputPath.replace(/\$([A-Z_][A-Z0-9_]*)/gi, (match, varName) => {
115
+ return process.env[varName] || match;
116
+ });
117
+ return normalize(resolve(inputPath));
118
+ }
119
+ /**
120
+ * Ensure all required directories exist
121
+ */
122
+ ensureDirectories() {
123
+ const dirs = [
124
+ this.baseDir,
125
+ this.domainsDir,
126
+ this.archivesDir
127
+ ];
128
+ for (const dir of dirs) {
129
+ if (!existsSync(dir)) {
130
+ mkdirSync(dir, { recursive: true });
131
+ }
132
+ }
133
+ }
134
+ /**
135
+ * Check if running on Windows
136
+ */
137
+ get isWindows() {
138
+ return this._platform === "win32";
139
+ }
140
+ /**
141
+ * Check if running on macOS
142
+ */
143
+ get isMacOS() {
144
+ return this._platform === "darwin";
145
+ }
146
+ /**
147
+ * Check if running on Linux
148
+ */
149
+ get isLinux() {
150
+ return this._platform === "linux";
151
+ }
152
+ /**
153
+ * Get platform name
154
+ */
155
+ get platform() {
156
+ return this._platform;
157
+ }
158
+ };
159
+ var paths = Paths.instance;
160
+
161
+ export {
162
+ paths
163
+ };
@@ -0,0 +1,26 @@
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
+ };
@@ -0,0 +1,116 @@
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
+ };
@@ -0,0 +1,303 @@
1
+ import {
2
+ paths
3
+ } from "./chunk-A7YXSI66.js";
4
+
5
+ // src/core/config.ts
6
+ import { readFileSync, existsSync, writeFileSync, readdirSync, renameSync, unlinkSync } from "fs";
7
+ import { parse, stringify } from "yaml";
8
+
9
+ // src/core/domain.ts
10
+ import { z } from "zod";
11
+ var MCPServerSchema = z.object({
12
+ command: z.string().describe("Command to execute the MCP server"),
13
+ args: z.array(z.string()).optional().describe("Arguments for the command"),
14
+ env: z.record(z.string()).optional().describe("Environment variables")
15
+ });
16
+ var DomainConfigSchema = z.object({
17
+ name: z.string().min(1).describe("Domain identifier"),
18
+ workingDirectory: z.string().describe("Working directory for this domain"),
19
+ claudeConfig: z.object({
20
+ instructions: z.string().optional().describe("Path to CLAUDE.md file"),
21
+ memory: z.array(z.string()).optional().describe("Memory file paths")
22
+ }).optional(),
23
+ mcpServers: z.record(MCPServerSchema).optional().describe("MCP server configurations"),
24
+ env: z.record(z.string()).optional().describe("Environment variables for this domain"),
25
+ extends: z.string().optional().describe("Parent domain to inherit from"),
26
+ metadata: z.object({
27
+ description: z.string().optional(),
28
+ created: z.string().datetime().optional(),
29
+ lastUsed: z.string().datetime().optional(),
30
+ tags: z.array(z.string()).optional()
31
+ }).optional()
32
+ });
33
+ var StateSchema = z.object({
34
+ version: z.literal("1.0.0"),
35
+ activeDomain: z.string().nullable(),
36
+ activeSession: z.string().nullable(),
37
+ lastSwitch: z.string().datetime().optional(),
38
+ sessions: z.record(z.object({
39
+ domain: z.string(),
40
+ sessionId: z.string(),
41
+ started: z.string().datetime(),
42
+ lastActive: z.string().datetime(),
43
+ processId: z.number().optional()
44
+ })).optional()
45
+ });
46
+ var GlobalConfigSchema = z.object({
47
+ version: z.literal("1.0.0"),
48
+ defaultDomain: z.string().default("default"),
49
+ autoArchive: z.boolean().default(true),
50
+ archiveDirectory: z.string().optional(),
51
+ claudePath: z.string().optional().describe("Path to Claude CLI executable"),
52
+ logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
53
+ platform: z.object({
54
+ forceType: z.enum(["darwin", "linux", "win32"]).optional(),
55
+ killSignal: z.enum(["SIGTERM", "SIGKILL"]).default("SIGTERM"),
56
+ killTimeout: z.number().default(5e3).describe("Milliseconds to wait before force kill")
57
+ }).optional()
58
+ });
59
+ var DEFAULT_DOMAIN_TEMPLATE = {
60
+ name: "default",
61
+ workingDirectory: process.cwd(),
62
+ claudeConfig: {
63
+ instructions: "./CLAUDE.md"
64
+ },
65
+ mcpServers: {},
66
+ env: {},
67
+ metadata: {
68
+ description: "Default domain",
69
+ created: (/* @__PURE__ */ new Date()).toISOString(),
70
+ tags: []
71
+ }
72
+ };
73
+ function validateDomain(data) {
74
+ return DomainConfigSchema.parse(data);
75
+ }
76
+ function validateState(data) {
77
+ return StateSchema.parse(data);
78
+ }
79
+ function createInitialState() {
80
+ return {
81
+ version: "1.0.0",
82
+ activeDomain: null,
83
+ activeSession: null,
84
+ sessions: {}
85
+ };
86
+ }
87
+
88
+ // src/core/config.ts
89
+ var ConfigManager = class {
90
+ globalConfig = null;
91
+ state = null;
92
+ domainCache = /* @__PURE__ */ new Map();
93
+ /**
94
+ * Load global configuration
95
+ */
96
+ loadGlobalConfig() {
97
+ if (this.globalConfig) {
98
+ return this.globalConfig;
99
+ }
100
+ const configPath = paths.globalConfigFile;
101
+ if (!existsSync(configPath)) {
102
+ this.globalConfig = {
103
+ version: "1.0.0",
104
+ defaultDomain: "default",
105
+ autoArchive: true,
106
+ logLevel: "info"
107
+ };
108
+ this.saveGlobalConfig(this.globalConfig);
109
+ return this.globalConfig;
110
+ }
111
+ try {
112
+ const content = readFileSync(configPath, "utf-8");
113
+ const data = parse(content);
114
+ this.globalConfig = GlobalConfigSchema.parse(data);
115
+ return this.globalConfig;
116
+ } catch (error) {
117
+ throw new Error(`Failed to load global config: ${error}`);
118
+ }
119
+ }
120
+ /**
121
+ * Save global configuration
122
+ */
123
+ saveGlobalConfig(config) {
124
+ const configPath = paths.globalConfigFile;
125
+ const content = stringify(config, { indent: 2 });
126
+ writeFileSync(configPath, content, "utf-8");
127
+ this.globalConfig = config;
128
+ }
129
+ /**
130
+ * Load state file
131
+ */
132
+ loadState() {
133
+ if (this.state) {
134
+ return this.state;
135
+ }
136
+ const statePath = paths.stateFile;
137
+ if (!existsSync(statePath)) {
138
+ this.state = createInitialState();
139
+ this.saveState(this.state);
140
+ return this.state;
141
+ }
142
+ try {
143
+ const content = readFileSync(statePath, "utf-8");
144
+ const data = JSON.parse(content);
145
+ this.state = validateState(data);
146
+ return this.state;
147
+ } catch (error) {
148
+ throw new Error(`Failed to load state: ${error}`);
149
+ }
150
+ }
151
+ /**
152
+ * Save state file (atomic write)
153
+ */
154
+ saveState(state) {
155
+ const statePath = paths.stateFile;
156
+ const tempPath = `${statePath}.tmp`;
157
+ const content = JSON.stringify(state, null, 2);
158
+ writeFileSync(tempPath, content, "utf-8");
159
+ renameSync(tempPath, statePath);
160
+ this.state = state;
161
+ }
162
+ /**
163
+ * Load a domain configuration
164
+ */
165
+ loadDomain(domainName) {
166
+ if (this.domainCache.has(domainName)) {
167
+ return this.domainCache.get(domainName);
168
+ }
169
+ const domainPath = paths.domainConfigFile(domainName);
170
+ if (!existsSync(domainPath)) {
171
+ throw new Error(`Domain '${domainName}' not found`);
172
+ }
173
+ try {
174
+ const content = readFileSync(domainPath, "utf-8");
175
+ const data = parse(content);
176
+ let domain = validateDomain(data);
177
+ if (domain.extends) {
178
+ const parent = this.loadDomain(domain.extends);
179
+ domain = this.mergeDomains(parent, domain);
180
+ }
181
+ domain.workingDirectory = paths.expandPath(domain.workingDirectory);
182
+ if (domain.claudeConfig?.instructions) {
183
+ domain.claudeConfig.instructions = paths.expandPath(domain.claudeConfig.instructions);
184
+ }
185
+ if (domain.claudeConfig?.memory) {
186
+ domain.claudeConfig.memory = domain.claudeConfig.memory.map((p) => paths.expandPath(p));
187
+ }
188
+ this.domainCache.set(domainName, domain);
189
+ return domain;
190
+ } catch (error) {
191
+ throw new Error(`Failed to load domain '${domainName}': ${error}`);
192
+ }
193
+ }
194
+ /**
195
+ * Save a domain configuration
196
+ */
197
+ saveDomain(domain) {
198
+ const domainPath = paths.domainConfigFile(domain.name);
199
+ const content = stringify(domain, { indent: 2 });
200
+ writeFileSync(domainPath, content, "utf-8");
201
+ this.domainCache.set(domain.name, domain);
202
+ }
203
+ /**
204
+ * Create a new domain from template
205
+ */
206
+ createDomain(name, template) {
207
+ const domain = {
208
+ ...DEFAULT_DOMAIN_TEMPLATE,
209
+ ...template,
210
+ name,
211
+ metadata: {
212
+ ...DEFAULT_DOMAIN_TEMPLATE.metadata,
213
+ ...template?.metadata,
214
+ created: (/* @__PURE__ */ new Date()).toISOString()
215
+ }
216
+ };
217
+ this.saveDomain(domain);
218
+ return domain;
219
+ }
220
+ /**
221
+ * List all available domains
222
+ */
223
+ listDomains() {
224
+ const domainsDir = paths.domainsDir;
225
+ if (!existsSync(domainsDir)) {
226
+ return [];
227
+ }
228
+ return readdirSync(domainsDir).filter((file) => file.endsWith(".yml")).map((file) => file.replace(".yml", ""));
229
+ }
230
+ /**
231
+ * Check if a domain exists
232
+ */
233
+ domainExists(domainName) {
234
+ const domainPath = paths.domainConfigFile(domainName);
235
+ return existsSync(domainPath);
236
+ }
237
+ /**
238
+ * Delete a domain
239
+ */
240
+ deleteDomain(domainName) {
241
+ const domainPath = paths.domainConfigFile(domainName);
242
+ if (!existsSync(domainPath)) {
243
+ throw new Error(`Domain '${domainName}' not found`);
244
+ }
245
+ unlinkSync(domainPath);
246
+ this.domainCache.delete(domainName);
247
+ }
248
+ /**
249
+ * Merge two domain configurations (child overrides parent)
250
+ */
251
+ mergeDomains(parent, child) {
252
+ const merged = {
253
+ ...parent,
254
+ ...child,
255
+ name: child.name || parent.name,
256
+ workingDirectory: child.workingDirectory || parent.workingDirectory
257
+ };
258
+ if (parent.claudeConfig || child.claudeConfig) {
259
+ merged.claudeConfig = {
260
+ ...parent.claudeConfig,
261
+ ...child.claudeConfig
262
+ };
263
+ if (parent.claudeConfig?.memory || child.claudeConfig?.memory) {
264
+ merged.claudeConfig.memory = [
265
+ ...parent.claudeConfig?.memory || [],
266
+ ...child.claudeConfig?.memory || []
267
+ ];
268
+ }
269
+ }
270
+ if (parent.mcpServers || child.mcpServers) {
271
+ merged.mcpServers = {
272
+ ...parent.mcpServers,
273
+ ...child.mcpServers
274
+ };
275
+ }
276
+ if (parent.env || child.env) {
277
+ merged.env = {
278
+ ...parent.env,
279
+ ...child.env
280
+ };
281
+ }
282
+ if (parent.metadata || child.metadata) {
283
+ merged.metadata = {
284
+ ...parent.metadata,
285
+ ...child.metadata
286
+ };
287
+ }
288
+ return merged;
289
+ }
290
+ /**
291
+ * Clear cache
292
+ */
293
+ clearCache() {
294
+ this.globalConfig = null;
295
+ this.state = null;
296
+ this.domainCache.clear();
297
+ }
298
+ };
299
+ var configManager = new ConfigManager();
300
+
301
+ export {
302
+ configManager
303
+ };