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
|
@@ -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
|
+
};
|