contextwise 0.1.0 → 0.2.0

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.
@@ -1,329 +1,14 @@
1
- // src/config/schema.ts
2
- import { z } from "zod";
3
- var StdioUpstreamConfigSchema = z.object({
4
- command: z.string(),
5
- args: z.array(z.string()).default([]),
6
- env: z.record(z.string()).default({}),
7
- cwd: z.string().optional(),
8
- autoRestart: z.boolean().default(true)
9
- });
10
- var HttpUpstreamConfigSchema = z.object({
11
- url: z.string().url(),
12
- headers: z.record(z.string()).default({}),
13
- transport: z.enum(["streamable-http", "sse", "auto"]).default("auto"),
14
- autoReconnect: z.boolean().default(true)
15
- });
16
- var UpstreamServerConfigSchema = z.union([
17
- StdioUpstreamConfigSchema,
18
- HttpUpstreamConfigSchema
19
- ]);
20
- function warnIfInsecureHttp(urlStr, headers) {
21
- try {
22
- const parsed = new URL(urlStr);
23
- if (parsed.protocol === "http:") {
24
- const isLocalhost = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(parsed.hostname);
25
- if (!isLocalhost && headers) {
26
- const hasAuthHeader = Object.keys(headers).some(
27
- (h) => ["authorization", "cookie", "x-api-key", "api-key", "token"].includes(h.toLowerCase())
28
- );
29
- if (hasAuthHeader) {
30
- console.warn(
31
- `[SECURITY WARNING] Upstream URL "${urlStr}" is using unencrypted HTTP with sensitive authentication headers to non-localhost destination "${parsed.hostname}".`
32
- );
33
- }
34
- }
35
- }
36
- } catch {
37
- }
38
- }
39
- function isStdioUpstream(config) {
40
- return "command" in config;
41
- }
42
- function isHttpUpstream(config) {
43
- return "url" in config;
44
- }
45
- var ProxyConfigSchema = z.object({
46
- transport: z.enum(["stdio", "sse", "http"]).default("stdio"),
47
- port: z.number().int().min(1024).max(65535).default(3456),
48
- logLevel: z.enum(["debug", "info", "warn", "error", "silent"]).default("info")
49
- });
50
- var RoutingConfigSchema = z.object({
51
- strategy: z.enum(["hybrid", "bm25", "vector", "passthrough"]).default("hybrid"),
52
- topK: z.number().int().min(1).max(50).default(5),
53
- similarityThreshold: z.number().min(0).max(1).default(0.45),
54
- pinnedTools: z.array(z.string()).default([]),
55
- maxActiveTools: z.number().int().min(1).max(100).default(10),
56
- enableBrowseServers: z.boolean().default(false),
57
- enableAddServer: z.boolean().default(false),
58
- allowCustomCommands: z.boolean().default(false),
59
- persistAddedServers: z.boolean().default(false)
60
- });
61
- var GuardrailsConfigSchema = z.object({
62
- enableCache: z.boolean().default(true),
63
- cacheTtlSeconds: z.number().int().min(1).default(120),
64
- maxCallsPerMinute: z.number().int().min(1).default(60),
65
- loopBreakerThreshold: z.number().int().min(1).default(3),
66
- callTimeoutMs: z.number().int().min(1e3).default(3e4)
67
- });
68
- var ContextWiseConfigSchema = z.object({
69
- $schema: z.string().optional(),
70
- version: z.string().default("1.0.0"),
71
- proxy: ProxyConfigSchema.default({}),
72
- routing: RoutingConfigSchema.default({}),
73
- guardrails: GuardrailsConfigSchema.default({}),
74
- upstreams: z.record(UpstreamServerConfigSchema).default({})
75
- });
76
-
77
- // src/vault/redaction.ts
78
- var RedactionFilter = class _RedactionFilter {
79
- static instance = null;
80
- registeredSecrets = /* @__PURE__ */ new Set();
81
- static getInstance() {
82
- if (!_RedactionFilter.instance) {
83
- _RedactionFilter.instance = new _RedactionFilter();
84
- }
85
- return _RedactionFilter.instance;
86
- }
87
- /**
88
- * Registers a plaintext secret value to be masked across all output streams.
89
- */
90
- registerSecret(secret) {
91
- if (secret && typeof secret === "string") {
92
- const trimmed = secret.trim();
93
- if (trimmed.length >= 4) {
94
- this.registeredSecrets.add(trimmed);
95
- }
96
- }
97
- }
98
- registerSecrets(secrets) {
99
- for (const s of secrets) {
100
- this.registerSecret(s);
101
- }
102
- }
103
- getRegisteredCount() {
104
- return this.registeredSecrets.size;
105
- }
106
- clear() {
107
- this.registeredSecrets.clear();
108
- }
109
- /**
110
- * Redacts registered secrets and known API key patterns from an input string.
111
- */
112
- redact(input) {
113
- if (!input || typeof input !== "string") {
114
- return input;
115
- }
116
- let sanitized = input;
117
- for (const secret of this.registeredSecrets) {
118
- sanitized = sanitized.replaceAll(secret, "[REDACTED_SECRET]");
119
- }
120
- sanitized = sanitized.replace(/ghp_[a-zA-Z0-9]{36}/g, "ghp_[REDACTED_GITHUB_TOKEN]");
121
- sanitized = sanitized.replace(/sk-ant-[a-zA-Z0-9_-]{20,}/g, "sk-ant-[REDACTED_ANTHROPIC_KEY]");
122
- sanitized = sanitized.replace(/sk-[a-zA-Z0-9]{32,}/g, "sk-[REDACTED_OPENAI_KEY]");
123
- sanitized = sanitized.replace(
124
- /([a-zA-Z0-9+]+:\/\/[^:]+:)([^@]+)(@.+)/g,
125
- "$1[REDACTED_PASSWORD]$3"
126
- );
127
- return sanitized;
128
- }
129
- };
130
- var redactionFilter = RedactionFilter.getInstance();
131
-
132
- // src/utils/logger.ts
133
- var LOG_LEVELS = {
134
- debug: 10,
135
- info: 20,
136
- warn: 30,
137
- error: 40,
138
- silent: 100
139
- };
140
- var Logger = class {
141
- level = "info";
142
- prefix;
143
- constructor(prefix = "ContextWise", level = "info") {
144
- this.prefix = prefix;
145
- this.level = level;
146
- }
147
- setLevel(level) {
148
- this.level = level;
149
- }
150
- getLevel() {
151
- return this.level;
152
- }
153
- shouldLog(level) {
154
- return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
155
- }
156
- formatMessage(level, message, meta) {
157
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
158
- const metaStr = meta !== void 0 ? ` ${typeof meta === "object" ? JSON.stringify(meta) : String(meta)}` : "";
159
- const raw = `[${timestamp}] [${level.toUpperCase()}] [${this.prefix}] ${message}${metaStr}
160
- `;
161
- return redactionFilter.redact(raw);
162
- }
163
- debug(message, meta) {
164
- if (this.shouldLog("debug")) {
165
- process.stderr.write(this.formatMessage("DEBUG", message, meta));
166
- }
167
- }
168
- info(message, meta) {
169
- if (this.shouldLog("info")) {
170
- process.stderr.write(this.formatMessage("INFO", message, meta));
171
- }
172
- }
173
- warn(message, meta) {
174
- if (this.shouldLog("warn")) {
175
- process.stderr.write(this.formatMessage("WARN", message, meta));
176
- }
177
- }
178
- error(message, meta) {
179
- if (this.shouldLog("error")) {
180
- process.stderr.write(this.formatMessage("ERROR", message, meta));
181
- }
182
- }
183
- };
184
- var logger = new Logger("ContextWise");
185
-
186
- // src/config/loader.ts
187
- import { existsSync, readFileSync } from "fs";
188
- import { homedir } from "os";
189
- import { isAbsolute, join, resolve } from "path";
190
- var ConfigLoader = class {
191
- /**
192
- * Discovers and loads configuration from contextwise.json or imported client configs.
193
- */
194
- static load(options = {}) {
195
- const cwd = options.cwd ?? process.cwd();
196
- if (process.env.CONTEXTWISE_CONFIG && !options.configPath) {
197
- const envPath = isAbsolute(process.env.CONTEXTWISE_CONFIG) ? process.env.CONTEXTWISE_CONFIG : resolve(cwd, process.env.CONTEXTWISE_CONFIG);
198
- if (existsSync(envPath)) {
199
- return this.parseConfigFile(envPath);
200
- }
201
- }
202
- if (options.configPath) {
203
- const explicitPath = isAbsolute(options.configPath) ? options.configPath : resolve(cwd, options.configPath);
204
- if (existsSync(explicitPath)) {
205
- return this.parseConfigFile(explicitPath);
206
- }
207
- throw new Error(`Configuration file not found at: ${explicitPath}`);
208
- }
209
- const candidatePaths = [
210
- resolve(cwd, "contextwise.json"),
211
- resolve(cwd, ".contextwise.json"),
212
- resolve(cwd, ".contextwise/config.json"),
213
- resolve(homedir(), ".contextwise/config.json")
214
- ];
215
- for (const candidate of candidatePaths) {
216
- if (existsSync(candidate)) {
217
- logger.debug(`Loaded configuration from ${candidate}`);
218
- return this.parseConfigFile(candidate);
219
- }
220
- }
221
- if (options.autoImport !== false) {
222
- const imported = this.autoImportClientConfigs(cwd);
223
- if (imported && Object.keys(imported.upstreams).length > 0) {
224
- logger.info(`Auto-discovered MCP servers from existing client configurations`);
225
- return imported;
226
- }
227
- }
228
- logger.debug("No configuration file found. Using defaults.");
229
- return ContextWiseConfigSchema.parse({});
230
- }
231
- /**
232
- * Parses and validates a JSON file against ContextWiseConfigSchema.
233
- */
234
- static parseConfigFile(filePath) {
235
- try {
236
- const raw = readFileSync(filePath, "utf-8");
237
- const parsed = JSON.parse(raw);
238
- return ContextWiseConfigSchema.parse(parsed);
239
- } catch (err) {
240
- const msg = err instanceof Error ? err.message : String(err);
241
- throw new Error(`Failed to parse config at ${filePath}: ${msg}`);
242
- }
243
- }
244
- /**
245
- * Scans for Cursor and Claude Desktop configs to auto-import upstreams.
246
- */
247
- static autoImportClientConfigs(cwd) {
248
- const upstreams = {};
249
- const cursorMcp = resolve(cwd, ".cursor", "mcp.json");
250
- if (existsSync(cursorMcp)) {
251
- try {
252
- const raw = JSON.parse(readFileSync(cursorMcp, "utf-8"));
253
- if (raw.mcpServers && typeof raw.mcpServers === "object") {
254
- for (const [name, server] of Object.entries(raw.mcpServers)) {
255
- if (name === "contextwise") continue;
256
- const s = server;
257
- if (typeof s.command === "string") {
258
- upstreams[name] = {
259
- command: s.command,
260
- args: Array.isArray(s.args) ? s.args : [],
261
- env: s.env ?? {},
262
- autoRestart: true
263
- };
264
- } else if (typeof s.url === "string") {
265
- upstreams[name] = {
266
- url: s.url,
267
- headers: s.headers ?? {},
268
- transport: "auto",
269
- autoReconnect: true
270
- };
271
- }
272
- }
273
- }
274
- } catch (err) {
275
- logger.warn(`Failed reading ${cursorMcp}: ${err}`);
276
- }
277
- }
278
- const claudePath = this.getClaudeDesktopConfigPath();
279
- if (claudePath && existsSync(claudePath)) {
280
- try {
281
- const raw = JSON.parse(readFileSync(claudePath, "utf-8"));
282
- if (raw.mcpServers && typeof raw.mcpServers === "object") {
283
- for (const [name, server] of Object.entries(raw.mcpServers)) {
284
- if (name === "contextwise") continue;
285
- const s = server;
286
- if (typeof s.command === "string" && !upstreams[name]) {
287
- upstreams[name] = {
288
- command: s.command,
289
- args: Array.isArray(s.args) ? s.args : [],
290
- env: s.env ?? {},
291
- autoRestart: true
292
- };
293
- } else if (typeof s.url === "string" && !upstreams[name]) {
294
- upstreams[name] = {
295
- url: s.url,
296
- headers: s.headers ?? {},
297
- transport: "auto",
298
- autoReconnect: true
299
- };
300
- }
301
- }
302
- }
303
- } catch (err) {
304
- logger.warn(`Failed reading Claude config at ${claudePath}: ${err}`);
305
- }
306
- }
307
- if (Object.keys(upstreams).length === 0) {
308
- return null;
309
- }
310
- return ContextWiseConfigSchema.parse({
311
- upstreams
312
- });
313
- }
314
- static getClaudeDesktopConfigPath() {
315
- const platform3 = process.platform;
316
- const home = homedir();
317
- if (platform3 === "win32") {
318
- const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
319
- return join(appData, "Claude", "claude_desktop_config.json");
320
- } else if (platform3 === "darwin") {
321
- return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
322
- } else {
323
- return join(home, ".config", "Claude", "claude_desktop_config.json");
324
- }
325
- }
326
- };
1
+ import {
2
+ ContextWiseConfigSchema,
3
+ EncryptedFileVaultDriver,
4
+ isHttpUpstream,
5
+ isStdioUpstream,
6
+ warnIfInsecureHttp
7
+ } from "./chunk-OYCA37PJ.js";
8
+ import {
9
+ logger,
10
+ redactionFilter
11
+ } from "./chunk-WP7RH2Z2.js";
327
12
 
328
13
  // src/core/supervisor.ts
329
14
  import { execSync } from "child_process";
@@ -419,239 +104,9 @@ var ProcessSupervisor = class _ProcessSupervisor {
419
104
  };
420
105
  var supervisor = ProcessSupervisor.getInstance();
421
106
 
422
- // src/vault/crypto.ts
423
- import {
424
- createCipheriv,
425
- createDecipheriv,
426
- createHash,
427
- createPrivateKey,
428
- createPublicKey,
429
- diffieHellman,
430
- generateKeyPairSync,
431
- pbkdf2,
432
- randomBytes
433
- } from "crypto";
434
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
435
- import { arch, homedir as homedir2, hostname, platform, userInfo } from "os";
436
- import { join as join2 } from "path";
437
- function encryptAesGcm(plaintext, key) {
438
- if (key.length !== 32) {
439
- throw new Error(`Invalid AES-256 key length: expected 32 bytes, got ${key.length}`);
440
- }
441
- const iv = randomBytes(12);
442
- const cipher = createCipheriv("aes-256-gcm", key, iv);
443
- let ciphertext = cipher.update(plaintext, "utf-8", "base64");
444
- ciphertext += cipher.final("base64");
445
- const authTag = cipher.getAuthTag();
446
- return {
447
- iv: iv.toString("base64"),
448
- authTag: authTag.toString("base64"),
449
- ciphertext
450
- };
451
- }
452
- function decryptAesGcm(ciphertext, key, ivBase64, authTagBase64) {
453
- if (key.length !== 32) {
454
- throw new Error(`Invalid AES-256 key length: expected 32 bytes, got ${key.length}`);
455
- }
456
- const iv = Buffer.from(ivBase64, "base64");
457
- const authTag = Buffer.from(authTagBase64, "base64");
458
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
459
- decipher.setAuthTag(authTag);
460
- let decrypted = decipher.update(ciphertext, "base64", "utf-8");
461
- decrypted += decipher.final("utf-8");
462
- return decrypted;
463
- }
464
- var VAULT_PBKDF2_ITERATIONS = 21e4;
465
- function deriveKeyFromPassphrase(passphrase, salt, iterations = VAULT_PBKDF2_ITERATIONS) {
466
- return new Promise((resolve4, reject) => {
467
- pbkdf2(passphrase, salt, iterations, 32, "sha512", (err, derivedKey) => {
468
- if (err) reject(err);
469
- else resolve4(derivedKey);
470
- });
471
- });
472
- }
473
- async function getOrCreateMachineKey() {
474
- const saltDir = join2(homedir2(), ".contextwise");
475
- const saltPath = join2(saltDir, ".machine_salt");
476
- let salt;
477
- if (existsSync2(saltPath)) {
478
- salt = readFileSync2(saltPath);
479
- } else {
480
- mkdirSync(saltDir, { recursive: true });
481
- salt = randomBytes(32);
482
- writeFileSync(saltPath, salt, { mode: 384 });
483
- }
484
- if (process.env.CONTEXTWISE_VAULT_PASSPHRASE) {
485
- return deriveKeyFromPassphrase(process.env.CONTEXTWISE_VAULT_PASSPHRASE, salt, VAULT_PBKDF2_ITERATIONS);
486
- }
487
- const user = (() => {
488
- try {
489
- return userInfo().username;
490
- } catch {
491
- return "default_user";
492
- }
493
- })();
494
- const machineId = `${hostname()}-${user}-${platform()}-${arch()}-contextwise-vault-v1`;
495
- return deriveKeyFromPassphrase(machineId, salt, VAULT_PBKDF2_ITERATIONS);
496
- }
497
- function generateKeyPairX25519() {
498
- const { publicKey, privateKey } = generateKeyPairSync("x25519", {
499
- publicKeyEncoding: { type: "spki", format: "pem" },
500
- privateKeyEncoding: { type: "pkcs8", format: "pem" }
501
- });
502
- return { publicKey, privateKey };
503
- }
504
- function deriveSharedSecretX25519(privateKeyPem, publicKeyPem) {
505
- const privateKey = createPrivateKey(privateKeyPem);
506
- const publicKey = createPublicKey(publicKeyPem);
507
- const shared = diffieHellman({
508
- privateKey,
509
- publicKey
510
- });
511
- return createHash("sha256").update(shared).digest();
512
- }
513
-
514
- // src/vault/file_driver.ts
515
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
516
- import { homedir as homedir3 } from "os";
517
- import { dirname as dirname2, join as join3 } from "path";
518
- function getDefaultVaultFilePath() {
519
- if (process.env.CONTEXTWISE_VAULT_PATH) {
520
- return process.env.CONTEXTWISE_VAULT_PATH;
521
- }
522
- if (process.env.CONTEXTWISE_STORAGE_DIR) {
523
- return join3(process.env.CONTEXTWISE_STORAGE_DIR, "vault.enc.json");
524
- }
525
- return join3(homedir3(), ".contextwise", "vault.enc.json");
526
- }
527
- var EncryptedFileVaultDriver = class {
528
- name = "encrypted_file";
529
- filePath;
530
- keyPromise;
531
- cache = null;
532
- constructor(filePath, customKey) {
533
- this.filePath = filePath ?? getDefaultVaultFilePath();
534
- this.keyPromise = customKey ? Promise.resolve(customKey) : getOrCreateMachineKey();
535
- }
536
- async isAvailable() {
537
- return true;
538
- }
539
- async load() {
540
- if (this.cache) {
541
- return this.cache;
542
- }
543
- this.cache = /* @__PURE__ */ new Map();
544
- if (!existsSync3(this.filePath)) {
545
- return this.cache;
546
- }
547
- try {
548
- const raw = readFileSync3(this.filePath, "utf-8");
549
- const payload = JSON.parse(raw);
550
- if (payload.cipher !== "aes-256-gcm" || !payload.data) {
551
- logger.warn(`Corrupt or incompatible vault file at ${this.filePath}`);
552
- return this.cache;
553
- }
554
- const key = await this.keyPromise;
555
- const decryptedJson = decryptAesGcm(payload.data, key, payload.iv, payload.authTag);
556
- const entries = JSON.parse(decryptedJson);
557
- for (const [k, v] of Object.entries(entries)) {
558
- this.cache.set(k, v);
559
- }
560
- } catch (err) {
561
- logger.warn(`Failed to decrypt vault file at ${this.filePath}: ${err}`);
562
- }
563
- return this.cache;
564
- }
565
- async persist() {
566
- if (!this.cache) return;
567
- const dir = dirname2(this.filePath);
568
- if (!existsSync3(dir)) {
569
- mkdirSync2(dir, { recursive: true });
570
- }
571
- const key = await this.keyPromise;
572
- const entries = {};
573
- for (const [k, v] of this.cache.entries()) {
574
- entries[k] = v;
575
- }
576
- const plaintext = JSON.stringify(entries);
577
- const { iv, authTag, ciphertext } = encryptAesGcm(plaintext, key);
578
- const payload = {
579
- version: 1,
580
- kdf: {
581
- algorithm: "pbkdf2-sha512",
582
- salt: "machine-bound",
583
- iterations: VAULT_PBKDF2_ITERATIONS
584
- },
585
- cipher: "aes-256-gcm",
586
- iv,
587
- authTag,
588
- data: ciphertext
589
- };
590
- const tempPath = `${this.filePath}.${Date.now()}.tmp`;
591
- writeFileSync2(tempPath, JSON.stringify(payload, null, 2), { mode: 384, encoding: "utf-8" });
592
- try {
593
- renameSync(tempPath, this.filePath);
594
- } catch {
595
- writeFileSync2(this.filePath, JSON.stringify(payload, null, 2), { mode: 384, encoding: "utf-8" });
596
- try {
597
- unlinkSync(tempPath);
598
- } catch {
599
- }
600
- }
601
- }
602
- async get(key) {
603
- const store = await this.load();
604
- const entry = store.get(key);
605
- return entry ? entry.value : null;
606
- }
607
- async set(key, value, scope = "personal") {
608
- const store = await this.load();
609
- const now = Date.now();
610
- const existing = store.get(key);
611
- store.set(key, {
612
- value,
613
- metadata: {
614
- key,
615
- scope,
616
- backend: this.name,
617
- createdAt: existing?.metadata.createdAt ?? now,
618
- updatedAt: now
619
- }
620
- });
621
- await this.persist();
622
- logger.debug(`Stored secret "${key}" in ${this.name} vault`);
623
- }
624
- async delete(key) {
625
- const store = await this.load();
626
- if (!store.has(key)) {
627
- return false;
628
- }
629
- store.delete(key);
630
- await this.persist();
631
- logger.debug(`Deleted secret "${key}" from ${this.name} vault`);
632
- return true;
633
- }
634
- async list() {
635
- const store = await this.load();
636
- return Array.from(store.values()).map((e) => e.metadata);
637
- }
638
- /**
639
- * Resets and clears all cached and persisted secrets.
640
- */
641
- async clear() {
642
- this.cache = /* @__PURE__ */ new Map();
643
- if (existsSync3(this.filePath)) {
644
- try {
645
- unlinkSync(this.filePath);
646
- } catch {
647
- }
648
- }
649
- }
650
- };
651
-
652
107
  // src/vault/os_driver.ts
653
108
  import { execFileSync } from "child_process";
654
- import { platform as platform2 } from "os";
109
+ import { platform } from "os";
655
110
  var OsKeystoreDriver = class {
656
111
  name = "os_keystore";
657
112
  available = null;
@@ -660,7 +115,7 @@ var OsKeystoreDriver = class {
660
115
  if (this.available !== null) {
661
116
  return this.available;
662
117
  }
663
- const currentPlatform = platform2();
118
+ const currentPlatform = platform();
664
119
  try {
665
120
  if (currentPlatform === "darwin") {
666
121
  execFileSync("/usr/bin/security", ["list-keychains"], { stdio: "ignore" });
@@ -678,7 +133,7 @@ var OsKeystoreDriver = class {
678
133
  if (!await this.isAvailable()) {
679
134
  return null;
680
135
  }
681
- const currentPlatform = platform2();
136
+ const currentPlatform = platform();
682
137
  try {
683
138
  if (currentPlatform === "darwin") {
684
139
  const stdout = execFileSync(
@@ -697,7 +152,7 @@ var OsKeystoreDriver = class {
697
152
  if (!await this.isAvailable()) {
698
153
  throw new Error("OS Keystore driver is not available on this system.");
699
154
  }
700
- const currentPlatform = platform2();
155
+ const currentPlatform = platform();
701
156
  if (currentPlatform === "darwin") {
702
157
  execFileSync(
703
158
  "/usr/bin/security",
@@ -712,7 +167,7 @@ var OsKeystoreDriver = class {
712
167
  if (!await this.isAvailable()) {
713
168
  return false;
714
169
  }
715
- const currentPlatform = platform2();
170
+ const currentPlatform = platform();
716
171
  try {
717
172
  if (currentPlatform === "darwin") {
718
173
  execFileSync(
@@ -889,9 +344,9 @@ var VaultAuditor = class {
889
344
  };
890
345
 
891
346
  // src/vault/resolver.ts
892
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
893
- import { homedir as homedir4 } from "os";
894
- import { join as join4 } from "path";
347
+ import { existsSync, readFileSync } from "fs";
348
+ import { homedir } from "os";
349
+ import { join } from "path";
895
350
  var SecretResolver = class {
896
351
  /**
897
352
  * Resolves a single secret token reference (vault://, auth://, env://).
@@ -927,10 +382,10 @@ var SecretResolver = class {
927
382
  if (!serverKey) {
928
383
  throw new Error('Invalid auth reference format: expected "auth://<server>"');
929
384
  }
930
- const mcpAuthPath = join4(homedir4(), ".local", "share", "opencode", "mcp-auth.json");
931
- if (existsSync4(mcpAuthPath)) {
385
+ const mcpAuthPath = join(homedir(), ".local", "share", "opencode", "mcp-auth.json");
386
+ if (existsSync(mcpAuthPath)) {
932
387
  try {
933
- const authData = JSON.parse(readFileSync4(mcpAuthPath, "utf-8"));
388
+ const authData = JSON.parse(readFileSync(mcpAuthPath, "utf-8"));
934
389
  const token = authData[serverKey]?.tokens?.accessToken;
935
390
  if (token) {
936
391
  redactionFilter.registerSecret(token);
@@ -1731,7 +1186,7 @@ var ToolRegistry = class {
1731
1186
  var toolRegistry = new ToolRegistry();
1732
1187
 
1733
1188
  // src/guardrails/validator.ts
1734
- import { createHash as createHash2 } from "crypto";
1189
+ import { createHash } from "crypto";
1735
1190
  import Ajv from "ajv";
1736
1191
  import addFormats from "ajv-formats";
1737
1192
  var ToolArgumentValidator = class {
@@ -1750,7 +1205,7 @@ var ToolArgumentValidator = class {
1750
1205
  }
1751
1206
  computeSchemaHash(schema) {
1752
1207
  try {
1753
- return createHash2("sha256").update(JSON.stringify(schema ?? "")).digest("hex").slice(0, 16);
1208
+ return createHash("sha256").update(JSON.stringify(schema ?? "")).digest("hex").slice(0, 16);
1754
1209
  } catch {
1755
1210
  return "static";
1756
1211
  }
@@ -1797,7 +1252,7 @@ var ToolArgumentValidator = class {
1797
1252
  var argumentValidator = new ToolArgumentValidator();
1798
1253
 
1799
1254
  // src/utils/hash.ts
1800
- import { createHash as createHash3 } from "crypto";
1255
+ import { createHash as createHash2 } from "crypto";
1801
1256
  function canonicalJsonStringify(obj, seen = /* @__PURE__ */ new WeakSet()) {
1802
1257
  if (obj === null || obj === void 0) {
1803
1258
  return "null";
@@ -1823,7 +1278,7 @@ function canonicalJsonStringify(obj, seen = /* @__PURE__ */ new WeakSet()) {
1823
1278
  return "{" + pairs.join(",") + "}";
1824
1279
  }
1825
1280
  function sha256(input) {
1826
- return createHash3("sha256").update(input).digest("hex");
1281
+ return createHash2("sha256").update(input).digest("hex");
1827
1282
  }
1828
1283
  function generateToolCallCacheKey(serverName, toolName, args = {}) {
1829
1284
  const canonicalArgs = canonicalJsonStringify(args);
@@ -2015,9 +1470,9 @@ var RunawayLoopBreaker = class {
2015
1470
  var loopBreaker = new RunawayLoopBreaker();
2016
1471
 
2017
1472
  // src/metrics/collector.ts
2018
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
2019
- import { homedir as homedir5 } from "os";
2020
- import { dirname as dirname3, join as join5 } from "path";
1473
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
1474
+ import { homedir as homedir2 } from "os";
1475
+ import { dirname, join as join2 } from "path";
2021
1476
  var SCHEMA_TOKENS_PER_TOOL = 150;
2022
1477
  var CACHE_TOKENS_PER_HIT = 800;
2023
1478
  var LOOP_PREVENTION_TOKENS_PER_TRIP = 2500;
@@ -2043,7 +1498,7 @@ function getDefaultMetricsPath() {
2043
1498
  if (process.env.CONTEXTWISE_METRICS_PATH) {
2044
1499
  return process.env.CONTEXTWISE_METRICS_PATH;
2045
1500
  }
2046
- return join5(homedir5(), ".contextwise", "metrics.json");
1501
+ return join2(homedir2(), ".contextwise", "metrics.json");
2047
1502
  }
2048
1503
  var MetricsCollector = class {
2049
1504
  storagePath;
@@ -2091,8 +1546,8 @@ var MetricsCollector = class {
2091
1546
  }
2092
1547
  loadFromDisk() {
2093
1548
  try {
2094
- if (existsSync5(this.storagePath)) {
2095
- const raw = readFileSync5(this.storagePath, "utf-8");
1549
+ if (existsSync2(this.storagePath)) {
1550
+ const raw = readFileSync2(this.storagePath, "utf-8");
2096
1551
  const parsed = JSON.parse(raw);
2097
1552
  if (parsed && typeof parsed === "object") {
2098
1553
  this.data = {
@@ -2126,17 +1581,17 @@ var MetricsCollector = class {
2126
1581
  this.debounceTimer = null;
2127
1582
  }
2128
1583
  try {
2129
- const dir = dirname3(this.storagePath);
2130
- if (!existsSync5(dir)) {
2131
- mkdirSync3(dir, { recursive: true });
1584
+ const dir = dirname(this.storagePath);
1585
+ if (!existsSync2(dir)) {
1586
+ mkdirSync(dir, { recursive: true });
2132
1587
  }
2133
1588
  const tempFile = `${this.storagePath}.${Date.now()}.tmp`;
2134
1589
  const json = JSON.stringify(this.data, null, 2);
2135
- writeFileSync3(tempFile, json, "utf-8");
2136
- renameSync2(tempFile, this.storagePath);
1590
+ writeFileSync(tempFile, json, "utf-8");
1591
+ renameSync(tempFile, this.storagePath);
2137
1592
  } catch {
2138
1593
  try {
2139
- writeFileSync3(this.storagePath, JSON.stringify(this.data, null, 2), "utf-8");
1594
+ writeFileSync(this.storagePath, JSON.stringify(this.data, null, 2), "utf-8");
2140
1595
  } catch (err) {
2141
1596
  logger.debug(`Failed persisting metrics: ${err}`);
2142
1597
  }
@@ -2272,6 +1727,9 @@ var SEARCH_TOOLS_NAME = "contextwise_search_tools";
2272
1727
  var EXECUTE_TOOL_NAME = "contextwise_execute_tool";
2273
1728
  var BROWSE_SERVERS_NAME = "contextwise_browse_servers";
2274
1729
  var ADD_SERVER_NAME = "contextwise_add_server";
1730
+ var CLOUD_PUSH_NAME = "contextwise_cloud_push";
1731
+ var CLOUD_PULL_NAME = "contextwise_cloud_pull";
1732
+ var SYNC_STATUS_NAME = "contextwise_sync_status";
2275
1733
  var SEARCH_TOOLS_DEFINITION = {
2276
1734
  name: SEARCH_TOOLS_NAME,
2277
1735
  description: "Searches ContextWise tool catalog for relevant tools based on natural language intent. Returns matched tool signatures, parameter descriptions, and automatically makes them available to invoke.",
@@ -2383,6 +1841,40 @@ var ADD_SERVER_DEFINITION = {
2383
1841
  required: ["name"]
2384
1842
  }
2385
1843
  };
1844
+ var CLOUD_PUSH_DEFINITION = {
1845
+ name: CLOUD_PUSH_NAME,
1846
+ description: "Pushes the host ContextWise workspace snapshot (config + encrypted vault) to ContextWise Cloud. Usage-metered via MCPaid.",
1847
+ inputSchema: {
1848
+ type: "object",
1849
+ properties: {
1850
+ workspaceId: {
1851
+ type: "string",
1852
+ description: "Optional workspace ID (defaults to the active workspace)"
1853
+ }
1854
+ }
1855
+ }
1856
+ };
1857
+ var CLOUD_PULL_DEFINITION = {
1858
+ name: CLOUD_PULL_NAME,
1859
+ description: "Pulls and applies the latest ContextWise Cloud workspace revision to the host. Usage-metered via MCPaid.",
1860
+ inputSchema: {
1861
+ type: "object",
1862
+ properties: {
1863
+ workspaceId: {
1864
+ type: "string",
1865
+ description: "Optional workspace ID (defaults to the active workspace)"
1866
+ }
1867
+ }
1868
+ }
1869
+ };
1870
+ var SYNC_STATUS_DEFINITION = {
1871
+ name: SYNC_STATUS_NAME,
1872
+ description: "Reports ContextWise Cloud sync status for the host instance (login state, workspace, local revision). Always free.",
1873
+ inputSchema: {
1874
+ type: "object",
1875
+ properties: {}
1876
+ }
1877
+ };
2386
1878
 
2387
1879
  // src/registry/known_servers.ts
2388
1880
  var KNOWN_SERVERS = [
@@ -2939,25 +2431,25 @@ var ServerRecommender = class {
2939
2431
  };
2940
2432
 
2941
2433
  // src/registry/manager.ts
2942
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2943
- import { dirname as dirname4, resolve as resolve2 } from "path";
2434
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2435
+ import { dirname as dirname2, resolve } from "path";
2944
2436
  var ServerManager = class {
2945
2437
  /**
2946
2438
  * Finds the path to contextwise.json in the current working directory.
2947
2439
  */
2948
2440
  static findConfigPath(customPath, cwd = process.cwd()) {
2949
2441
  if (customPath) {
2950
- return resolve2(cwd, customPath);
2442
+ return resolve(cwd, customPath);
2951
2443
  }
2952
2444
  if (process.env.CONTEXTWISE_CONFIG) {
2953
- return resolve2(cwd, process.env.CONTEXTWISE_CONFIG);
2445
+ return resolve(cwd, process.env.CONTEXTWISE_CONFIG);
2954
2446
  }
2955
- const standardPath = resolve2(cwd, "contextwise.json");
2956
- if (existsSync6(standardPath)) {
2447
+ const standardPath = resolve(cwd, "contextwise.json");
2448
+ if (existsSync3(standardPath)) {
2957
2449
  return standardPath;
2958
2450
  }
2959
- const dotPath = resolve2(cwd, ".contextwise.json");
2960
- if (existsSync6(dotPath)) {
2451
+ const dotPath = resolve(cwd, ".contextwise.json");
2452
+ if (existsSync3(dotPath)) {
2961
2453
  return dotPath;
2962
2454
  }
2963
2455
  return standardPath;
@@ -2967,14 +2459,14 @@ var ServerManager = class {
2967
2459
  */
2968
2460
  static loadConfig(configPath, cwd = process.cwd()) {
2969
2461
  const filePath = this.findConfigPath(configPath, cwd);
2970
- if (!existsSync6(filePath)) {
2462
+ if (!existsSync3(filePath)) {
2971
2463
  return {
2972
2464
  config: ContextWiseConfigSchema.parse({}),
2973
2465
  path: filePath
2974
2466
  };
2975
2467
  }
2976
2468
  try {
2977
- const raw = readFileSync6(filePath, "utf-8");
2469
+ const raw = readFileSync3(filePath, "utf-8");
2978
2470
  return {
2979
2471
  config: ContextWiseConfigSchema.parse(JSON.parse(raw)),
2980
2472
  path: filePath
@@ -2993,11 +2485,11 @@ var ServerManager = class {
2993
2485
  static saveUpstream(name, serverConfig, configPath, cwd = process.cwd()) {
2994
2486
  const { config, path: filePath } = this.loadConfig(configPath, cwd);
2995
2487
  config.upstreams[name] = serverConfig;
2996
- const dir = dirname4(filePath);
2997
- if (!existsSync6(dir)) {
2998
- mkdirSync4(dir, { recursive: true });
2488
+ const dir = dirname2(filePath);
2489
+ if (!existsSync3(dir)) {
2490
+ mkdirSync2(dir, { recursive: true });
2999
2491
  }
3000
- writeFileSync4(filePath, JSON.stringify(config, null, 2), "utf-8");
2492
+ writeFileSync2(filePath, JSON.stringify(config, null, 2), "utf-8");
3001
2493
  logger.info(`Persisted upstream "${name}" into ${filePath}`);
3002
2494
  return filePath;
3003
2495
  }
@@ -3010,15 +2502,15 @@ var ServerManager = class {
3010
2502
  return false;
3011
2503
  }
3012
2504
  delete config.upstreams[name];
3013
- writeFileSync4(filePath, JSON.stringify(config, null, 2), "utf-8");
2505
+ writeFileSync2(filePath, JSON.stringify(config, null, 2), "utf-8");
3014
2506
  logger.info(`Removed upstream "${name}" from ${filePath}`);
3015
2507
  return true;
3016
2508
  }
3017
2509
  };
3018
2510
 
3019
2511
  // src/router/workspace.ts
3020
- import { existsSync as existsSync7, readdirSync } from "fs";
3021
- import { resolve as resolve3 } from "path";
2512
+ import { existsSync as existsSync4, readdirSync } from "fs";
2513
+ import { resolve as resolve2 } from "path";
3022
2514
  var WorkspaceContextPrimer = class {
3023
2515
  /**
3024
2516
  * Scans a workspace directory to detect technical domains and pre-prime relevant tools.
@@ -3027,14 +2519,14 @@ var WorkspaceContextPrimer = class {
3027
2519
  const domains = /* @__PURE__ */ new Set();
3028
2520
  const queries = /* @__PURE__ */ new Set();
3029
2521
  try {
3030
- if (!existsSync7(dirPath)) {
2522
+ if (!existsSync4(dirPath)) {
3031
2523
  return { detectedDomains: [], suggestedQueries: [] };
3032
2524
  }
3033
- if (existsSync7(resolve3(dirPath, ".git"))) {
2525
+ if (existsSync4(resolve2(dirPath, ".git"))) {
3034
2526
  domains.add("git");
3035
2527
  queries.add("git status and diff tools");
3036
2528
  }
3037
- if (existsSync7(resolve3(dirPath, "Dockerfile")) || existsSync7(resolve3(dirPath, "docker-compose.yml")) || existsSync7(resolve3(dirPath, "compose.yaml"))) {
2529
+ if (existsSync4(resolve2(dirPath, "Dockerfile")) || existsSync4(resolve2(dirPath, "docker-compose.yml")) || existsSync4(resolve2(dirPath, "compose.yaml"))) {
3038
2530
  domains.add("docker");
3039
2531
  queries.add("docker and container tools");
3040
2532
  }
@@ -3072,6 +2564,7 @@ var ContextRouter = class {
3072
2564
  maxActiveTools: config.maxActiveTools ?? 10,
3073
2565
  enableBrowseServers: config.enableBrowseServers ?? false,
3074
2566
  enableAddServer: config.enableAddServer ?? false,
2567
+ enableCloudSync: config.enableCloudSync ?? true,
3075
2568
  allowCustomCommands: config.allowCustomCommands ?? false,
3076
2569
  persistAddedServers: config.persistAddedServers ?? false
3077
2570
  };
@@ -3170,6 +2663,9 @@ ${manifest}` : SEARCH_TOOLS_DEFINITION.description
3170
2663
  if (this.config.enableAddServer) {
3171
2664
  metaTools.push(ADD_SERVER_DEFINITION);
3172
2665
  }
2666
+ if (this.config.enableCloudSync) {
2667
+ metaTools.push(CLOUD_PUSH_DEFINITION, CLOUD_PULL_DEFINITION, SYNC_STATUS_DEFINITION);
2668
+ }
3173
2669
  const metaToolNames = new Set(metaTools.map((m) => m.name));
3174
2670
  const safeActiveTools = activeTools.filter((t) => !metaToolNames.has(t.name));
3175
2671
  return [...metaTools, ...safeActiveTools];
@@ -3247,15 +2743,33 @@ import {
3247
2743
  CallToolRequestSchema,
3248
2744
  ListToolsRequestSchema
3249
2745
  } from "@modelcontextprotocol/sdk/types.js";
2746
+ import { AsyncLocalStorage } from "async_hooks";
2747
+ import { parseAndVerifyEdgeReceipt, MemoryReceiptStore } from "@mcpaid/sdk";
2748
+ var requestContext = new AsyncLocalStorage();
3250
2749
  var ContextWiseProxy = class {
3251
2750
  server;
3252
2751
  multiplexer;
3253
2752
  router;
3254
2753
  transport;
2754
+ httpServers = [];
2755
+ httpTransports = [];
3255
2756
  config;
3256
2757
  isRunning = false;
2758
+ // Single-use receipt registry: bounded (FIFO cap) with expired-first eviction.
2759
+ claimedReceiptNonces = new MemoryReceiptStore();
3257
2760
  constructor() {
3258
- this.server = new Server(
2761
+ this.server = this.createSessionServer();
2762
+ this.multiplexer = new UpstreamMultiplexer();
2763
+ this.router = contextRouter;
2764
+ }
2765
+ /**
2766
+ * Builds a Session-scoped MCP Server whose handlers delegate to the shared
2767
+ * engine (router, multiplexer, cloud clients). One instance per HTTP
2768
+ * session — the MCP SDK binds a Server to a single transport, and sharing
2769
+ * one Server across sessions breaks repeat handshakes.
2770
+ */
2771
+ createSessionServer() {
2772
+ const server = new Server(
3259
2773
  {
3260
2774
  name: "contextwise",
3261
2775
  version: "0.1.0"
@@ -3268,12 +2782,12 @@ var ContextWiseProxy = class {
3268
2782
  }
3269
2783
  }
3270
2784
  );
3271
- this.multiplexer = new UpstreamMultiplexer();
3272
- this.router = contextRouter;
3273
- this.setupRequestHandlers();
2785
+ this.setupRequestHandlers(server);
2786
+ return server;
3274
2787
  }
3275
- setupRequestHandlers() {
3276
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
2788
+ setupRequestHandlers(server) {
2789
+ const target = server ?? this.server;
2790
+ target.setRequestHandler(ListToolsRequestSchema, async () => {
3277
2791
  const exposedTools = this.router.getExposedTools();
3278
2792
  const allToolsCount = toolRegistry.getAllTools().length;
3279
2793
  metricsCollector.updateToolCounts(allToolsCount, exposedTools.length);
@@ -3285,7 +2799,7 @@ var ContextWiseProxy = class {
3285
2799
  tools: exposedTools
3286
2800
  };
3287
2801
  });
3288
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
2802
+ target.setRequestHandler(CallToolRequestSchema, async (request) => {
3289
2803
  const { name, arguments: args = {} } = request.params;
3290
2804
  return this.handleToolCall(name, args);
3291
2805
  });
@@ -3293,7 +2807,7 @@ var ContextWiseProxy = class {
3293
2807
  /**
3294
2808
  * Main tool execution pipeline handling meta-tools and proxied calls.
3295
2809
  */
3296
- async handleToolCall(name, args = {}) {
2810
+ async handleToolCall(name, args = {}, receipt) {
3297
2811
  if (name === SEARCH_TOOLS_NAME) {
3298
2812
  return this.handleSearchToolsCall(args);
3299
2813
  }
@@ -3319,6 +2833,15 @@ var ContextWiseProxy = class {
3319
2833
  if (name === ADD_SERVER_NAME) {
3320
2834
  return this.handleAddServerCall(args);
3321
2835
  }
2836
+ if (name === CLOUD_PUSH_NAME) {
2837
+ return this.handleCloudPushCall(args, receipt);
2838
+ }
2839
+ if (name === CLOUD_PULL_NAME) {
2840
+ return this.handleCloudPullCall(args, receipt);
2841
+ }
2842
+ if (name === SYNC_STATUS_NAME) {
2843
+ return this.handleSyncStatusCall();
2844
+ }
3322
2845
  return this.dispatchProxiedToolCall(name, args);
3323
2846
  }
3324
2847
  /**
@@ -3576,6 +3099,68 @@ These tools are now indexed, ${persistMsg}and ready for immediate invocation in
3576
3099
  ]
3577
3100
  };
3578
3101
  }
3102
+ /**
3103
+ * Handles metered cloud-sync meta-tool calls. These operate on the host
3104
+ * instance's workspace via its stored ContextWise Cloud session.
3105
+ */
3106
+ async handleCloudPushCall(args, receipt) {
3107
+ const { syncManager } = await import("./sync_manager-2WYXXGCN.js");
3108
+ const { cloudClient } = await import("./client-3GQWPGI3.js");
3109
+ if (!cloudClient.isAuthenticated()) {
3110
+ return {
3111
+ isError: true,
3112
+ content: [{ type: "text", text: 'Error: Host instance is not logged in to ContextWise Cloud. Run "contextwise login" on the host.' }]
3113
+ };
3114
+ }
3115
+ try {
3116
+ const workspaceId = typeof args.workspaceId === "string" ? args.workspaceId : void 0;
3117
+ const effectiveReceipt = receipt || requestContext.getStore()?.receipt;
3118
+ const res = await syncManager.push(workspaceId, effectiveReceipt);
3119
+ if (res.status === "committed") {
3120
+ return { content: [{ type: "text", text: `Cloud sync push committed as revision #${res.revision}.` }] };
3121
+ }
3122
+ return {
3123
+ isError: true,
3124
+ content: [{ type: "text", text: `Push conflict: server has revision #${res.serverRevision}. Pull first, then retry.` }]
3125
+ };
3126
+ } catch (err) {
3127
+ const msg = err instanceof Error ? err.message : String(err);
3128
+ return { isError: true, content: [{ type: "text", text: redactionFilter.redact(`Cloud push failed: ${msg}`) }] };
3129
+ }
3130
+ }
3131
+ async handleCloudPullCall(args, receipt) {
3132
+ const { syncManager } = await import("./sync_manager-2WYXXGCN.js");
3133
+ const { cloudClient } = await import("./client-3GQWPGI3.js");
3134
+ if (!cloudClient.isAuthenticated()) {
3135
+ return {
3136
+ isError: true,
3137
+ content: [{ type: "text", text: 'Error: Host instance is not logged in to ContextWise Cloud. Run "contextwise login" on the host.' }]
3138
+ };
3139
+ }
3140
+ try {
3141
+ const workspaceId = typeof args.workspaceId === "string" ? args.workspaceId : void 0;
3142
+ const effectiveReceipt = receipt || requestContext.getStore()?.receipt;
3143
+ const res = await syncManager.pull(workspaceId, effectiveReceipt);
3144
+ if (res) {
3145
+ return { content: [{ type: "text", text: `Applied cloud revision #${res.revision} to host workspace.` }] };
3146
+ }
3147
+ return { content: [{ type: "text", text: "Host workspace is already up to date." }] };
3148
+ } catch (err) {
3149
+ const msg = err instanceof Error ? err.message : String(err);
3150
+ return { isError: true, content: [{ type: "text", text: redactionFilter.redact(`Cloud pull failed: ${msg}`) }] };
3151
+ }
3152
+ }
3153
+ async handleSyncStatusCall() {
3154
+ const { syncManager } = await import("./sync_manager-2WYXXGCN.js");
3155
+ const status = syncManager.getStatus();
3156
+ const lines = [
3157
+ `Logged in: ${status.isLoggedIn ? "yes" : "no"}`,
3158
+ `Workspace: ${status.workspaceId}`,
3159
+ `Local revision: ${status.revision}`,
3160
+ `Last synced: ${status.lastSyncedAt > 0 ? new Date(status.lastSyncedAt).toISOString() : "never"}`
3161
+ ];
3162
+ return { content: [{ type: "text", text: lines.join("\n") }] };
3163
+ }
3579
3164
  /**
3580
3165
  * Dispatches tool call through guardrails and upstream multiplexer.
3581
3166
  */
@@ -3698,9 +3283,9 @@ ${validation.errors?.join("\n")}`;
3698
3283
  }
3699
3284
  }
3700
3285
  /**
3701
- * Starts ContextWise proxy using given configuration.
3286
+ * Shared boot: config, guardrails, upstreams, catalog. Transport-agnostic.
3702
3287
  */
3703
- async start(config) {
3288
+ async boot(config) {
3704
3289
  this.config = config;
3705
3290
  this.isRunning = true;
3706
3291
  logger.setLevel(config.proxy.logLevel);
@@ -3723,605 +3308,225 @@ ${validation.errors?.join("\n")}`;
3723
3308
  }
3724
3309
  });
3725
3310
  this.router.primeWorkspace(process.cwd());
3726
- this.transport = new StdioServerTransport();
3727
- await this.server.connect(this.transport);
3728
- logger.info("ContextWise MCP Proxy is running and ready for client connections.");
3729
3311
  }
3730
3312
  /**
3731
- * Gracefully shuts down proxy and upstream connections.
3313
+ * Starts ContextWise proxy over stdio (default: local AI clients).
3732
3314
  */
3733
- async stop() {
3734
- if (!this.isRunning) return;
3735
- this.isRunning = false;
3736
- metricsCollector.flushSync();
3737
- logger.info("Shutting down ContextWise proxy...");
3738
- await this.multiplexer.closeAll();
3739
- if (this.transport) {
3740
- await this.transport.close();
3741
- }
3742
- await this.server.close();
3743
- logger.info("ContextWise proxy stopped cleanly.");
3744
- }
3745
- getMultiplexer() {
3746
- return this.multiplexer;
3747
- }
3748
- getRouter() {
3749
- return this.router;
3750
- }
3751
- };
3752
- var proxy = new ContextWiseProxy();
3753
-
3754
- // src/cloud/client.ts
3755
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
3756
- import { homedir as homedir6 } from "os";
3757
- import { join as join6 } from "path";
3758
- var ContextWiseCloudClient = class {
3759
- apiUrl;
3760
- storageDir;
3761
- allowOfflineSimulation;
3762
- token = null;
3763
- constructor(options = {}) {
3764
- this.apiUrl = options.apiUrl || process.env.CONTEXTWISE_API_URL || "https://contextwise.dev";
3765
- this.storageDir = options.storageDir || process.env.CONTEXTWISE_STORAGE_DIR || join6(homedir6(), ".contextwise");
3766
- this.allowOfflineSimulation = options.allowOfflineSimulation ?? false;
3767
- this.loadToken();
3768
- }
3769
- getTokenPath() {
3770
- return join6(this.storageDir, "auth.json");
3771
- }
3772
- loadToken() {
3773
- const tokenPath = this.getTokenPath();
3774
- if (existsSync8(tokenPath)) {
3775
- try {
3776
- const raw = readFileSync7(tokenPath, "utf-8");
3777
- this.token = JSON.parse(raw);
3778
- } catch {
3779
- this.token = null;
3780
- }
3781
- }
3782
- }
3783
- saveToken(token) {
3784
- if (!existsSync8(this.storageDir)) {
3785
- mkdirSync5(this.storageDir, { recursive: true });
3786
- }
3787
- this.token = token;
3788
- writeFileSync5(this.getTokenPath(), JSON.stringify(token, null, 2), {
3789
- mode: 384,
3790
- encoding: "utf-8"
3791
- });
3792
- }
3793
- clearToken() {
3794
- this.token = null;
3795
- const tokenPath = this.getTokenPath();
3796
- if (existsSync8(tokenPath)) {
3797
- try {
3798
- unlinkSync2(tokenPath);
3799
- } catch {
3800
- }
3801
- }
3802
- }
3803
- getToken() {
3804
- if (!this.token) {
3805
- this.loadToken();
3806
- }
3807
- return this.token;
3808
- }
3809
- isAuthenticated() {
3810
- const token = this.getToken();
3811
- if (!token) return false;
3812
- return token.expiresAt > Date.now();
3315
+ async start(config) {
3316
+ await this.boot(config);
3317
+ this.transport = new StdioServerTransport();
3318
+ await this.server.connect(this.transport);
3319
+ logger.info("ContextWise MCP Proxy is running and ready for client connections.");
3813
3320
  }
3814
3321
  /**
3815
- * Starts RFC 8628 device authorization flow.
3322
+ * Starts ContextWise proxy over Streamable HTTP (for MCPaid edge publishing
3323
+ * via `mcpaid publish`, or any remote MCP client). Stateless: each request
3324
+ * gets its own transport, so no session affinity is required.
3816
3325
  */
3817
- async startDeviceFlow() {
3818
- const res = await fetch(`${this.apiUrl}/v1/auth/device/code`, {
3819
- method: "POST",
3820
- headers: { "Content-Type": "application/json" }
3821
- });
3822
- if (!res.ok) {
3823
- throw new Error(`Failed to initiate device login: HTTP ${res.status}`);
3326
+ async startHttp(config, options = {}) {
3327
+ const { createServer } = await import("http");
3328
+ let StreamableHTTPServerTransport;
3329
+ try {
3330
+ ({ StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js"));
3331
+ } catch {
3332
+ throw new Error(
3333
+ "HTTP transport requires @modelcontextprotocol/sdk with Streamable HTTP support. Run: npm install @modelcontextprotocol/sdk@latest"
3334
+ );
3824
3335
  }
3825
- return await res.json();
3826
- }
3827
- /**
3828
- * Polls device token until user approves the code in their browser.
3829
- */
3830
- async pollDeviceToken(deviceCode, intervalSeconds = 5, maxWaitMs = 15 * 60 * 1e3) {
3831
- const start = Date.now();
3832
- const intervalMs = Math.max(intervalSeconds * 1e3, 2e3);
3833
- while (Date.now() - start < maxWaitMs) {
3834
- await new Promise((r) => setTimeout(r, intervalMs));
3336
+ await this.boot(config);
3337
+ const port = options.port ?? 3e3;
3338
+ const mcpPath = options.path ?? "/mcp";
3339
+ const { randomUUID } = await import("crypto");
3340
+ const { isInitializeRequest } = await import("@modelcontextprotocol/sdk/types.js");
3341
+ const httpSessions = /* @__PURE__ */ new Map();
3342
+ this.httpTransports.push(httpSessions);
3343
+ const httpServer = createServer(async (req, res) => {
3835
3344
  try {
3836
- const res = await fetch(`${this.apiUrl}/v1/auth/device/token`, {
3837
- method: "POST",
3838
- headers: { "Content-Type": "application/json" },
3839
- body: JSON.stringify({ device_code: deviceCode })
3840
- });
3841
- if (res.ok) {
3842
- const data = await res.json();
3843
- const authToken = {
3844
- token: data.token,
3845
- userId: data.userId,
3846
- email: data.email,
3847
- expiresAt: data.expiresAt
3848
- };
3849
- this.saveToken(authToken);
3850
- return authToken;
3345
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
3346
+ if (url.pathname !== mcpPath) {
3347
+ res.writeHead(404, { "Content-Type": "application/json" });
3348
+ res.end(JSON.stringify({ error: "not_found", message: `Expected POST ${mcpPath}` }));
3349
+ return;
3851
3350
  }
3852
- if (res.status === 428) {
3853
- continue;
3351
+ if (req.method !== "POST" && req.method !== "GET" && req.method !== "DELETE") {
3352
+ res.writeHead(405, { "Content-Type": "application/json" });
3353
+ res.end(JSON.stringify({ error: "method_not_allowed" }));
3354
+ return;
3854
3355
  }
3855
- const errData = await res.json().catch(() => ({}));
3856
- throw new Error(errData.message || `Device authorization failed: HTTP ${res.status}`);
3857
- } catch (err) {
3858
- if (err instanceof Error && err.message.includes("Device authorization failed")) {
3859
- throw err;
3356
+ let body;
3357
+ if (req.method === "POST") {
3358
+ const chunks = [];
3359
+ for await (const chunk of req) chunks.push(chunk);
3360
+ try {
3361
+ body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
3362
+ } catch {
3363
+ res.writeHead(400, { "Content-Type": "application/json" });
3364
+ res.end(JSON.stringify({ error: "invalid_json" }));
3365
+ return;
3366
+ }
3860
3367
  }
3861
- }
3862
- }
3863
- throw new Error("Device authorization timed out. Please try logging in again.");
3864
- }
3865
- /**
3866
- * Authenticates using a user API token or personal access key.
3867
- */
3868
- async loginWithKey(apiKey) {
3869
- const trimmed = apiKey.trim();
3870
- if (!trimmed) {
3871
- throw new Error("API key cannot be empty");
3872
- }
3873
- try {
3874
- const res = await fetch(`${this.apiUrl}/v1/auth/verify`, {
3875
- method: "POST",
3876
- headers: {
3877
- "Content-Type": "application/json",
3878
- Authorization: `Bearer ${trimmed}`
3368
+ const rawReceipt = req.headers["x-mcpaid-receipt"];
3369
+ const receiptHeader = Array.isArray(rawReceipt) ? rawReceipt[0] : rawReceipt;
3370
+ const receiptSecret = (process.env.MCPAID_RECEIPT_SECRET || "").trim();
3371
+ const previousReceiptSecret = (process.env.MCPAID_PREVIOUS_RECEIPT_SECRET || "").trim();
3372
+ if (receiptSecret && body && typeof body === "object") {
3373
+ const rpcRequests = Array.isArray(body) ? body : [body];
3374
+ for (const rpc of rpcRequests) {
3375
+ if (rpc && typeof rpc === "object" && rpc.method === "tools/call" && rpc.params?.name) {
3376
+ const toolName = String(rpc.params.name);
3377
+ if (toolName === CLOUD_PUSH_NAME || toolName === CLOUD_PULL_NAME) {
3378
+ const minAmountMicro = toolName === CLOUD_PUSH_NAME ? 10000n : 5000n;
3379
+ const verifyResult = await parseAndVerifyEdgeReceipt(
3380
+ {
3381
+ secret: receiptSecret,
3382
+ previousSecret: previousReceiptSecret || void 0,
3383
+ store: this.claimedReceiptNonces
3384
+ },
3385
+ {
3386
+ receipt: receiptHeader,
3387
+ expectedServer: "contextwise",
3388
+ expectedTool: toolName,
3389
+ minAmountMicro
3390
+ }
3391
+ );
3392
+ if (!verifyResult.valid) {
3393
+ logger.warn(`Rejected unverified tool call [${toolName}]: ${verifyResult.error}`);
3394
+ res.writeHead(402, { "Content-Type": "application/json" });
3395
+ res.end(
3396
+ JSON.stringify({
3397
+ jsonrpc: "2.0",
3398
+ id: rpc.id ?? null,
3399
+ error: {
3400
+ code: -32002,
3401
+ message: `Payment Required: ${verifyResult.error}`
3402
+ }
3403
+ })
3404
+ );
3405
+ return;
3406
+ }
3407
+ }
3408
+ }
3409
+ }
3879
3410
  }
3880
- });
3881
- if (!res.ok) {
3882
- if (res.status === 401 || res.status === 403) {
3883
- throw new Error("Invalid or expired ContextWise Cloud API key.");
3411
+ const isGatewayOrigin = req.headers["x-mcpaid-gateway"] === "true";
3412
+ const rawSession = req.headers["mcp-session-id"];
3413
+ const sessionId = Array.isArray(rawSession) ? rawSession[0] : rawSession;
3414
+ let session = sessionId && httpSessions.get(sessionId) || void 0;
3415
+ if (!session && isGatewayOrigin && req.method === "POST") {
3416
+ const transport = new StreamableHTTPServerTransport({
3417
+ sessionIdGenerator: void 0
3418
+ });
3419
+ const server = this.createSessionServer();
3420
+ session = { transport, server };
3421
+ await server.connect(transport);
3422
+ try {
3423
+ await requestContext.run({ receipt: receiptHeader }, async () => {
3424
+ await session.transport.handleRequest(req, res, body);
3425
+ });
3426
+ } finally {
3427
+ try {
3428
+ await transport.close();
3429
+ } catch {
3430
+ }
3431
+ try {
3432
+ await server.close();
3433
+ } catch {
3434
+ }
3435
+ }
3436
+ return;
3884
3437
  }
3885
- throw new Error(`Cloud API returned HTTP ${res.status}: ${res.statusText}`);
3886
- }
3887
- const data = await res.json();
3888
- const authToken = {
3889
- token: trimmed,
3890
- userId: data.userId,
3891
- email: data.email,
3892
- expiresAt: data.expiresAt || Date.now() + 30 * 24 * 3600 * 1e3
3893
- };
3894
- this.saveToken(authToken);
3895
- return authToken;
3896
- } catch (err) {
3897
- if (this.allowOfflineSimulation && (trimmed.startsWith("cw_test_") || trimmed.startsWith("cw_live_"))) {
3898
- const dummyToken = {
3899
- token: trimmed,
3900
- userId: "usr_dev_" + trimmed.slice(-6),
3901
- email: "developer@contextwise.dev",
3902
- expiresAt: Date.now() + 30 * 24 * 3600 * 1e3
3903
- };
3904
- this.saveToken(dummyToken);
3905
- return dummyToken;
3906
- }
3907
- throw err;
3908
- }
3909
- }
3910
- /**
3911
- * Retrieves profile information for current authenticated account.
3912
- */
3913
- async whoami() {
3914
- const token = this.getToken();
3915
- if (!token) {
3916
- throw new Error('Not logged in. Run "contextwise login" first.');
3917
- }
3918
- try {
3919
- const res = await fetch(`${this.apiUrl}/v1/user/profile`, {
3920
- headers: { Authorization: `Bearer ${token.token}` }
3921
- });
3922
- if (res.ok) {
3923
- return await res.json();
3924
- }
3925
- if (!this.allowOfflineSimulation) {
3926
- throw new Error(`Failed to fetch profile: HTTP ${res.status} ${res.statusText}`);
3927
- }
3928
- } catch (err) {
3929
- if (!this.allowOfflineSimulation) {
3930
- throw err;
3931
- }
3932
- }
3933
- return {
3934
- userId: token.userId,
3935
- email: token.email,
3936
- plan: "free",
3937
- subscriptionStatus: "inactive",
3938
- currentPeriodEnd: null,
3939
- workspaces: [
3940
- {
3941
- id: "ws_default",
3942
- name: "Personal Workspace",
3943
- role: "owner",
3944
- activeRevision: 1,
3945
- updatedAt: Date.now()
3438
+ if (!session) {
3439
+ if (req.method === "POST" && body && typeof body === "object" && isInitializeRequest(body)) {
3440
+ const transport = new StreamableHTTPServerTransport({
3441
+ sessionIdGenerator: () => randomUUID(),
3442
+ onsessioninitialized: (sid) => {
3443
+ httpSessions.set(sid, session);
3444
+ }
3445
+ });
3446
+ const server = this.createSessionServer();
3447
+ session = { transport, server };
3448
+ transport.onclose = () => {
3449
+ const sid = transport.sessionId;
3450
+ if (sid && httpSessions.get(sid)?.transport === transport) {
3451
+ httpSessions.delete(sid);
3452
+ }
3453
+ };
3454
+ await server.connect(transport);
3455
+ } else {
3456
+ res.writeHead(400, { "Content-Type": "application/json" });
3457
+ res.end(
3458
+ JSON.stringify({
3459
+ jsonrpc: "2.0",
3460
+ id: null,
3461
+ error: { code: -32e3, message: "Bad Request: No valid session ID provided" }
3462
+ })
3463
+ );
3464
+ return;
3465
+ }
3946
3466
  }
3947
- ],
3948
- devices: []
3949
- };
3950
- }
3951
- /**
3952
- * Initiates Stripe Checkout session for Pro or Team upgrade.
3953
- */
3954
- async createCheckoutSession(plan = "pro", interval = "month") {
3955
- const token = this.getToken();
3956
- if (!token) {
3957
- throw new Error('Not logged in. Run "contextwise login" first.');
3958
- }
3959
- const res = await fetch(`${this.apiUrl}/v1/billing/checkout`, {
3960
- method: "POST",
3961
- headers: {
3962
- "Content-Type": "application/json",
3963
- Authorization: `Bearer ${token.token}`
3964
- },
3965
- body: JSON.stringify({ plan, interval })
3966
- });
3967
- if (!res.ok) {
3968
- const errData = await res.json().catch(() => ({}));
3969
- throw new Error(errData.message || `Failed to create checkout session: HTTP ${res.status}`);
3970
- }
3971
- return await res.json();
3972
- }
3973
- /**
3974
- * Generates Stripe Customer Portal session to manage subscription and invoices.
3975
- */
3976
- async createPortalSession() {
3977
- const token = this.getToken();
3978
- if (!token) {
3979
- throw new Error('Not logged in. Run "contextwise login" first.');
3980
- }
3981
- const res = await fetch(`${this.apiUrl}/v1/billing/portal`, {
3982
- method: "POST",
3983
- headers: {
3984
- "Content-Type": "application/json",
3985
- Authorization: `Bearer ${token.token}`
3986
- }
3987
- });
3988
- if (!res.ok) {
3989
- const errData = await res.json().catch(() => ({}));
3990
- throw new Error(errData.message || `Failed to open billing portal: HTTP ${res.status}`);
3991
- }
3992
- return await res.json();
3993
- }
3994
- /**
3995
- * Pushes a workspace snapshot (config + encrypted vault) to the Cloudflare Workers / D1 backend.
3996
- */
3997
- async pushSync(payload) {
3998
- const token = this.getToken();
3999
- if (!token) {
4000
- throw new Error('Not logged in. Run "contextwise login" first.');
4001
- }
4002
- try {
4003
- const res = await fetch(`${this.apiUrl}/v1/sync/push`, {
4004
- method: "POST",
4005
- headers: {
4006
- "Content-Type": "application/json",
4007
- Authorization: `Bearer ${token.token}`
4008
- },
4009
- body: JSON.stringify(payload)
4010
- });
4011
- if (res.ok) {
4012
- return await res.json();
4013
- }
4014
- if (res.status === 402) {
4015
- const payData = await res.json().catch(() => ({}));
4016
- throw new Error(
4017
- `${payData.message || "Cloud Sync requires an upgraded subscription."}
4018
- Upgrade at: ${payData.upgradeUrl || "https://contextwise.dev/#pricing"}`
4019
- );
4020
- }
4021
- if (res.status === 409) {
4022
- const conflictData = await res.json();
4023
- return {
4024
- status: "conflict",
4025
- revision: payload.baseRevision,
4026
- serverRevision: conflictData.serverRevision,
4027
- serverPayload: conflictData.serverPayload
4028
- };
4029
- }
4030
- throw new Error(`Push sync failed: HTTP ${res.status} ${res.statusText}`);
4031
- } catch (err) {
4032
- if (err instanceof Error && (err.message.includes("requires an upgraded subscription") || err.message.includes("Cloud Sync requires"))) {
4033
- throw err;
4034
- }
4035
- if (this.allowOfflineSimulation) {
4036
- logger.debug(`Cloud API push endpoint unreachable (${err}). Using local snapshot commit.`);
4037
- return {
4038
- status: "committed",
4039
- revision: payload.baseRevision + 1
4040
- };
4041
- }
4042
- throw err;
4043
- }
4044
- }
4045
- /**
4046
- * Pulls the latest workspace snapshot (config + encrypted vault) from the cloud backend.
4047
- */
4048
- async pullSync(workspaceId, sinceRevision = 0) {
4049
- const token = this.getToken();
4050
- if (!token) {
4051
- throw new Error('Not logged in. Run "contextwise login" first.');
4052
- }
4053
- try {
4054
- const res = await fetch(
4055
- `${this.apiUrl}/v1/sync/pull?workspaceId=${encodeURIComponent(
4056
- workspaceId
4057
- )}&since=${sinceRevision}`,
4058
- {
4059
- headers: { Authorization: `Bearer ${token.token}` }
3467
+ await requestContext.run({ receipt: receiptHeader }, async () => {
3468
+ await session.transport.handleRequest(req, res, body);
3469
+ });
3470
+ } catch (err) {
3471
+ const msg = err instanceof Error ? err.message : String(err);
3472
+ logger.error(`HTTP transport error: ${msg}`);
3473
+ if (!res.headersSent) {
3474
+ res.writeHead(500, { "Content-Type": "application/json" });
3475
+ }
3476
+ try {
3477
+ res.end(JSON.stringify({ error: "internal_error", message: msg }));
3478
+ } catch {
4060
3479
  }
4061
- );
4062
- if (res.ok) {
4063
- return await res.json();
4064
- }
4065
- if (res.status === 304) {
4066
- return null;
4067
- }
4068
- } catch (err) {
4069
- logger.debug(`Cloud API pull endpoint unreachable: ${err}`);
4070
- }
4071
- return null;
4072
- }
4073
- };
4074
- var cloudClient = new ContextWiseCloudClient();
4075
-
4076
- // src/cloud/crypto.ts
4077
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
4078
- import { homedir as homedir7 } from "os";
4079
- import { join as join7 } from "path";
4080
- import { randomBytes as randomBytes2 } from "crypto";
4081
- function createEnvelope(workspaceKey, recipientPublicKeyPem) {
4082
- const ephemeral = generateKeyPairX25519();
4083
- const sharedKey = deriveSharedSecretX25519(ephemeral.privateKey, recipientPublicKeyPem);
4084
- const { iv, authTag, ciphertext } = encryptAesGcm(
4085
- workspaceKey.toString("base64"),
4086
- sharedKey
4087
- );
4088
- return {
4089
- recipientPublicKey: recipientPublicKeyPem,
4090
- ephemeralPublicKey: ephemeral.publicKey,
4091
- iv,
4092
- authTag,
4093
- ciphertext
4094
- };
4095
- }
4096
- function openEnvelope(envelope, recipientPrivateKeyPem) {
4097
- const sharedKey = deriveSharedSecretX25519(
4098
- recipientPrivateKeyPem,
4099
- envelope.ephemeralPublicKey
4100
- );
4101
- const decryptedBase64 = decryptAesGcm(
4102
- envelope.ciphertext,
4103
- sharedKey,
4104
- envelope.iv,
4105
- envelope.authTag
4106
- );
4107
- return Buffer.from(decryptedBase64, "base64");
4108
- }
4109
- function getOrCreateDeviceIdentity(storageDir) {
4110
- const dir = storageDir || process.env.CONTEXTWISE_STORAGE_DIR || join7(homedir7(), ".contextwise");
4111
- const filePath = join7(dir, "device.json");
4112
- if (existsSync9(filePath)) {
4113
- try {
4114
- const data = JSON.parse(readFileSync8(filePath, "utf-8"));
4115
- if (data.deviceId && data.publicKey && data.privateKey) {
4116
- return data;
4117
- }
4118
- } catch {
4119
- }
4120
- }
4121
- if (!existsSync9(dir)) {
4122
- mkdirSync6(dir, { recursive: true });
4123
- }
4124
- const { publicKey, privateKey } = generateKeyPairX25519();
4125
- const deviceId = `cw_dev_${randomBytes2(8).toString("hex")}`;
4126
- const identity = {
4127
- deviceId,
4128
- publicKey,
4129
- privateKey
4130
- };
4131
- writeFileSync6(filePath, JSON.stringify(identity, null, 2), {
4132
- mode: 384,
4133
- encoding: "utf-8"
4134
- });
4135
- return identity;
4136
- }
4137
-
4138
- // src/cloud/sync_manager.ts
4139
- import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
4140
- import { homedir as homedir8 } from "os";
4141
- import { join as join8 } from "path";
4142
- var SyncManager = class {
4143
- syncStatePath;
4144
- localConfigPath;
4145
- constructor(storageDir, localConfigPath) {
4146
- const dir = storageDir || process.env.CONTEXTWISE_STORAGE_DIR || join8(homedir8(), ".contextwise");
4147
- this.syncStatePath = join8(dir, "sync.json");
4148
- this.localConfigPath = localConfigPath;
4149
- }
4150
- loadSyncState() {
4151
- if (existsSync10(this.syncStatePath)) {
4152
- try {
4153
- const raw = readFileSync9(this.syncStatePath, "utf-8");
4154
- return JSON.parse(raw);
4155
- } catch {
4156
3480
  }
4157
- }
4158
- return {
4159
- workspaceId: "ws_default",
4160
- revision: 0,
4161
- lastSyncedAt: 0
4162
- };
4163
- }
4164
- saveSyncState(state) {
4165
- writeFileSync7(this.syncStatePath, JSON.stringify(state, null, 2), {
4166
- mode: 384,
4167
- encoding: "utf-8"
4168
3481
  });
3482
+ await new Promise((resolve3) => httpServer.listen(port, "127.0.0.1", resolve3));
3483
+ this.httpServers.push(httpServer);
3484
+ logger.info(`ContextWise MCP Proxy listening on http://127.0.0.1:${port}${mcpPath} (Streamable HTTP).`);
3485
+ return httpServer;
4169
3486
  }
4170
3487
  /**
4171
- * Pushes local configuration and encrypted vault payload to the cloud.
4172
- */
4173
- async push(workspaceId) {
4174
- const state = this.loadSyncState();
4175
- const wsId = workspaceId || state.workspaceId;
4176
- const device = getOrCreateDeviceIdentity();
4177
- const localConfig = ConfigLoader.load();
4178
- const vaultPath = getDefaultVaultFilePath();
4179
- let encryptedVault;
4180
- if (existsSync10(vaultPath)) {
4181
- try {
4182
- const rawVault = readFileSync9(vaultPath, "utf-8");
4183
- encryptedVault = JSON.parse(rawVault);
4184
- } catch {
4185
- encryptedVault = this.createEmptyEncryptedVault();
4186
- }
4187
- } else {
4188
- encryptedVault = this.createEmptyEncryptedVault();
4189
- }
4190
- const payload = {
4191
- workspaceId: wsId,
4192
- deviceId: device.deviceId,
4193
- baseRevision: state.revision,
4194
- config: localConfig,
4195
- encryptedVault,
4196
- timestamp: Date.now()
4197
- };
4198
- const result = await cloudClient.pushSync(payload);
4199
- if (result.status === "committed") {
4200
- state.workspaceId = wsId;
4201
- state.revision = result.revision;
4202
- state.lastSyncedAt = Date.now();
4203
- this.saveSyncState(state);
4204
- logger.info(`Successfully pushed revision #${result.revision} to ContextWise Cloud.`);
4205
- }
4206
- return result;
4207
- }
4208
- /**
4209
- * Pulls the latest cloud configuration and merges upstream servers.
4210
- */
4211
- async pull(workspaceId) {
4212
- const state = this.loadSyncState();
4213
- const wsId = workspaceId || state.workspaceId;
4214
- const pullResult = await cloudClient.pullSync(wsId, state.revision);
4215
- if (!pullResult) {
4216
- logger.info("Local configuration is already up to date.");
4217
- return null;
4218
- }
4219
- if (pullResult.config && pullResult.config.upstreams) {
4220
- this.mergeConfigIntoLocal(pullResult.config);
4221
- }
4222
- if (pullResult.encryptedVault && pullResult.encryptedVault.data) {
4223
- const vaultPath = getDefaultVaultFilePath();
4224
- writeFileSync7(vaultPath, JSON.stringify(pullResult.encryptedVault, null, 2), {
4225
- mode: 384,
4226
- encoding: "utf-8"
4227
- });
4228
- }
4229
- state.revision = pullResult.revision;
4230
- state.lastSyncedAt = Date.now();
4231
- this.saveSyncState(state);
4232
- logger.info(`Pulled and applied cloud revision #${pullResult.revision}.`);
4233
- return pullResult;
4234
- }
4235
- /**
4236
- * Semantically merges remote configuration into local contextwise.json.
3488
+ * Gracefully shuts down proxy and upstream connections.
4237
3489
  */
4238
- mergeConfigIntoLocal(remoteConfig) {
4239
- const validatedRemote = ContextWiseConfigSchema.parse(remoteConfig);
4240
- const configPath = this.localConfigPath || process.env.CONTEXTWISE_CONFIG || join8(process.cwd(), "contextwise.json");
4241
- let localConfig;
4242
- if (existsSync10(configPath)) {
4243
- try {
4244
- localConfig = ContextWiseConfigSchema.parse(JSON.parse(readFileSync9(configPath, "utf-8")));
4245
- } catch {
4246
- localConfig = ContextWiseConfigSchema.parse({});
3490
+ async stop() {
3491
+ if (!this.isRunning) return;
3492
+ this.isRunning = false;
3493
+ metricsCollector.flushSync();
3494
+ logger.info("Shutting down ContextWise proxy...");
3495
+ await this.multiplexer.closeAll();
3496
+ for (const srv of this.httpServers) {
3497
+ await new Promise((resolve3) => srv.close(() => resolve3()));
3498
+ }
3499
+ this.httpServers = [];
3500
+ for (const t of this.httpTransports) {
3501
+ if (t instanceof Map) {
3502
+ for (const { transport } of t.values()) {
3503
+ await transport.close().catch(() => {
3504
+ });
3505
+ }
3506
+ } else {
3507
+ await t.close().catch(() => {
3508
+ });
4247
3509
  }
4248
- } else {
4249
- localConfig = ContextWiseConfigSchema.parse({});
4250
3510
  }
4251
- for (const [name, s] of Object.entries(validatedRemote.upstreams || {})) {
4252
- if (isStdioUpstream(s) && !localConfig.upstreams[name]) {
4253
- logger.warn(
4254
- `[Cloud Sync Security] Remote configuration contains new stdio upstream "${name}" (${s.command}). Verify this server in contextwise.json before execution.`
4255
- );
4256
- }
3511
+ this.httpTransports = [];
3512
+ if (this.transport) {
3513
+ await this.transport.close();
4257
3514
  }
4258
- const mergedUpstreams = {
4259
- ...validatedRemote.upstreams || {},
4260
- ...localConfig.upstreams || {}
4261
- };
4262
- const merged = {
4263
- ...localConfig,
4264
- upstreams: mergedUpstreams
4265
- };
4266
- writeFileSync7(configPath, JSON.stringify(merged, null, 2), "utf-8");
3515
+ await this.server.close();
3516
+ logger.info("ContextWise proxy stopped cleanly.");
4267
3517
  }
4268
- createEmptyEncryptedVault() {
4269
- return {
4270
- version: 1,
4271
- kdf: {
4272
- algorithm: "pbkdf2-sha512",
4273
- salt: "empty",
4274
- iterations: 5e4
4275
- },
4276
- cipher: "aes-256-gcm",
4277
- iv: "",
4278
- authTag: "",
4279
- data: ""
4280
- };
3518
+ getMultiplexer() {
3519
+ return this.multiplexer;
4281
3520
  }
4282
- getStatus() {
4283
- const token = cloudClient.getToken();
4284
- const state = this.loadSyncState();
4285
- const device = getOrCreateDeviceIdentity();
4286
- return {
4287
- isLoggedIn: cloudClient.isAuthenticated(),
4288
- userEmail: token?.email,
4289
- workspaceId: state.workspaceId,
4290
- revision: state.revision,
4291
- lastSyncedAt: state.lastSyncedAt,
4292
- deviceId: device.deviceId
4293
- };
3521
+ getRouter() {
3522
+ return this.router;
4294
3523
  }
4295
3524
  };
4296
- var syncManager = new SyncManager();
3525
+ var proxy = new ContextWiseProxy();
4297
3526
 
4298
3527
  export {
4299
- StdioUpstreamConfigSchema,
4300
- HttpUpstreamConfigSchema,
4301
- UpstreamServerConfigSchema,
4302
- warnIfInsecureHttp,
4303
- isStdioUpstream,
4304
- isHttpUpstream,
4305
- ProxyConfigSchema,
4306
- RoutingConfigSchema,
4307
- GuardrailsConfigSchema,
4308
- ContextWiseConfigSchema,
4309
- RedactionFilter,
4310
- redactionFilter,
4311
- Logger,
4312
- logger,
4313
- ConfigLoader,
4314
3528
  ProcessSupervisor,
4315
3529
  supervisor,
4316
- encryptAesGcm,
4317
- decryptAesGcm,
4318
- VAULT_PBKDF2_ITERATIONS,
4319
- deriveKeyFromPassphrase,
4320
- getOrCreateMachineKey,
4321
- generateKeyPairX25519,
4322
- deriveSharedSecretX25519,
4323
- getDefaultVaultFilePath,
4324
- EncryptedFileVaultDriver,
4325
3530
  OsKeystoreDriver,
4326
3531
  SecretVault,
4327
3532
  secretVault,
@@ -4356,10 +3561,16 @@ export {
4356
3561
  EXECUTE_TOOL_NAME,
4357
3562
  BROWSE_SERVERS_NAME,
4358
3563
  ADD_SERVER_NAME,
3564
+ CLOUD_PUSH_NAME,
3565
+ CLOUD_PULL_NAME,
3566
+ SYNC_STATUS_NAME,
4359
3567
  SEARCH_TOOLS_DEFINITION,
4360
3568
  EXECUTE_TOOL_DEFINITION,
4361
3569
  BROWSE_SERVERS_DEFINITION,
4362
3570
  ADD_SERVER_DEFINITION,
3571
+ CLOUD_PUSH_DEFINITION,
3572
+ CLOUD_PULL_DEFINITION,
3573
+ SYNC_STATUS_DEFINITION,
4363
3574
  KNOWN_SERVERS,
4364
3575
  KnownServerRegistry,
4365
3576
  OfficialRegistryClient,
@@ -4370,14 +3581,8 @@ export {
4370
3581
  WorkspaceContextPrimer,
4371
3582
  ContextRouter,
4372
3583
  contextRouter,
3584
+ requestContext,
4373
3585
  ContextWiseProxy,
4374
- proxy,
4375
- ContextWiseCloudClient,
4376
- cloudClient,
4377
- createEnvelope,
4378
- openEnvelope,
4379
- getOrCreateDeviceIdentity,
4380
- SyncManager,
4381
- syncManager
3586
+ proxy
4382
3587
  };
4383
- //# sourceMappingURL=chunk-P7JW7EPW.js.map
3588
+ //# sourceMappingURL=chunk-5ZMX5TEM.js.map