@gemus/mcp-proxy 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { open, readFile, rename, rm, lstat, access, mkdir } from 'node:fs/promises'
3
+ import { constants } from 'node:fs'
4
+ import { homedir as systemHomeDirectory } from 'node:os'
5
+ import path from 'node:path'
6
+
7
+ export class CodexConfigChangedError extends Error {
8
+ constructor(currentSource) {
9
+ super('Codex config changed while setup was running; re-plan from the latest config.')
10
+ this.code = 'CODEX_CONFIG_CHANGED'
11
+ this.currentSource = currentSource
12
+ }
13
+ }
14
+
15
+ function configPathFromEnvironment(env, homeDirectory) {
16
+ const configuredHome = typeof env.CODEX_HOME === 'string' && env.CODEX_HOME.trim() !== ''
17
+ ? env.CODEX_HOME
18
+ : undefined
19
+ const codexHome = configuredHome ?? path.join(homeDirectory(), '.codex')
20
+ if (!path.isAbsolute(codexHome)) throw new Error('CODEX_HOME must be an absolute path')
21
+ return path.join(codexHome, 'config.toml')
22
+ }
23
+
24
+ async function readSource(configPath, fileOps) {
25
+ try {
26
+ return await fileOps.readFile(configPath, 'utf8')
27
+ } catch (error) {
28
+ if (error?.code === 'ENOENT') return ''
29
+ throw error
30
+ }
31
+ }
32
+
33
+ async function validateConfigBoundary(configPath, fileOps) {
34
+ const root = path.parse(configPath).root
35
+ const parts = path.relative(root, configPath).split(path.sep).filter(Boolean)
36
+ let current = root
37
+ for (let index = 0; index < parts.length; index += 1) {
38
+ current = path.join(current, parts[index])
39
+ try {
40
+ const stat = await fileOps.lstat(current)
41
+ if (stat.isSymbolicLink()) throw new Error('refusing to replace a symlink Codex config path')
42
+ if (index < parts.length - 1 && !stat.isDirectory()) {
43
+ throw new Error('Codex config home path is not a directory')
44
+ }
45
+ } catch (error) {
46
+ if (error?.code === 'ENOENT') return
47
+ throw error
48
+ }
49
+ }
50
+ }
51
+
52
+ export async function preflightCodexConfig({
53
+ env = process.env,
54
+ fileOps: overrides,
55
+ homeDirectory = systemHomeDirectory,
56
+ } = {}) {
57
+ const fileOps = { readFile, lstat, access, ...overrides }
58
+ const configPath = configPathFromEnvironment(env, homeDirectory)
59
+ await validateConfigBoundary(configPath, fileOps)
60
+ let ancestor = path.dirname(configPath)
61
+ while (true) {
62
+ try { await fileOps.lstat(ancestor); break } catch (error) {
63
+ if (error?.code !== 'ENOENT') throw error
64
+ const parent = path.dirname(ancestor)
65
+ if (parent === ancestor) throw error
66
+ ancestor = parent
67
+ }
68
+ }
69
+ await fileOps.access(ancestor, constants.W_OK)
70
+ return { configPath, source: await readSource(configPath, fileOps) }
71
+ }
72
+
73
+ async function openExclusive(prefix, fileOps) {
74
+ for (let attempt = 0; attempt < 32; attempt += 1) {
75
+ const candidate = `${prefix}${randomUUID()}`
76
+ try {
77
+ return { path: candidate, handle: await fileOps.open(candidate, 'wx', 0o600) }
78
+ } catch (error) {
79
+ if (error?.code !== 'EEXIST') throw error
80
+ }
81
+ }
82
+ throw new Error('could not reserve a unique Codex config transaction file')
83
+ }
84
+
85
+ async function createConfigHomeSafely(configPath, fileOps) {
86
+ await validateConfigBoundary(configPath, fileOps)
87
+ const root = path.parse(configPath).root
88
+ const directories = path.relative(root, path.dirname(configPath)).split(path.sep).filter(Boolean)
89
+ let current = root
90
+ for (const segment of directories) {
91
+ current = path.join(current, segment)
92
+ try {
93
+ const stat = await fileOps.lstat(current)
94
+ if (stat.isSymbolicLink()) throw new Error('refusing to replace a symlink Codex config path')
95
+ if (!stat.isDirectory()) throw new Error('Codex config home path is not a directory')
96
+ } catch (error) {
97
+ if (error?.code !== 'ENOENT') throw error
98
+ await fileOps.mkdir(current, { mode: 0o700 })
99
+ const created = await fileOps.lstat(current)
100
+ if (created.isSymbolicLink()) throw new Error('refusing to replace a symlink Codex config path')
101
+ if (!created.isDirectory()) throw new Error('Codex config home path is not a directory')
102
+ }
103
+ }
104
+ await validateConfigBoundary(configPath, fileOps)
105
+ }
106
+
107
+ export async function commitCodexConfig({
108
+ expectedSource,
109
+ nextText,
110
+ env = process.env,
111
+ fileOps: overrides,
112
+ homeDirectory = systemHomeDirectory,
113
+ } = {}) {
114
+ if (typeof expectedSource !== 'string' || typeof nextText !== 'string') {
115
+ throw new TypeError('expectedSource and nextText must be strings')
116
+ }
117
+ const fileOps = { open, readFile, rename, rm, lstat, mkdir, ...overrides }
118
+ const configPath = configPathFromEnvironment(env, homeDirectory)
119
+ await createConfigHomeSafely(configPath, fileOps)
120
+ const currentSource = await readSource(configPath, fileOps)
121
+ if (currentSource !== expectedSource) throw new CodexConfigChangedError(currentSource)
122
+ if (currentSource === nextText) return { changed: false, backupPath: undefined }
123
+
124
+ let backupPath
125
+ let tempPath
126
+ try {
127
+ if (currentSource !== '') {
128
+ const backup = await openExclusive(`${configPath}.gemus-backup-`, fileOps)
129
+ backupPath = backup.path
130
+ try {
131
+ await backup.handle.writeFile(currentSource, 'utf8')
132
+ } finally {
133
+ await backup.handle.close()
134
+ }
135
+ }
136
+
137
+ const temp = await openExclusive(`${path.join(path.dirname(configPath), `.${path.basename(configPath)}.gemus-tmp-`)}`, fileOps)
138
+ tempPath = temp.path
139
+ try {
140
+ await temp.handle.writeFile(nextText, 'utf8')
141
+ } finally {
142
+ await temp.handle.close()
143
+ }
144
+ await validateConfigBoundary(configPath, fileOps)
145
+ const finalSource = await readSource(configPath, fileOps)
146
+ if (finalSource !== expectedSource) throw new CodexConfigChangedError(finalSource)
147
+ await fileOps.rename(tempPath, configPath)
148
+ tempPath = undefined
149
+ } catch (error) {
150
+ if (tempPath) await fileOps.rm(tempPath, { force: true })
151
+ throw error
152
+ }
153
+
154
+ return { changed: true, backupPath }
155
+ }
@@ -0,0 +1,79 @@
1
+ import { spawn } from 'node:child_process'
2
+
3
+ const WINDOWS_SCRIPT = [
4
+ "$ErrorActionPreference = 'Stop'",
5
+ 'try {',
6
+ " if ($env:GEMUS_URL) { [Environment]::SetEnvironmentVariable('GEMUS_URL', $env:GEMUS_URL, 'User') }",
7
+ " [Environment]::SetEnvironmentVariable('GEMUS_KEY', $env:GEMUS_KEY, 'User')",
8
+ "$signature = @'",
9
+ 'using System;',
10
+ 'using System.Runtime.InteropServices;',
11
+ 'public static class GemusEnvironmentBroadcast {',
12
+ ' [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]',
13
+ ' public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint msg, UIntPtr wParam, string lParam, uint flags, uint timeout, out UIntPtr result);',
14
+ '}',
15
+ "'@",
16
+ 'Add-Type -TypeDefinition $signature',
17
+ '$result = [UIntPtr]::Zero',
18
+ '[GemusEnvironmentBroadcast]::SendMessageTimeout([IntPtr]0xffff, 0x001A, [UIntPtr]::Zero, "Environment", 2, 5000, [ref]$result) | Out-Null # WM_SETTINGCHANGE',
19
+ '} finally {',
20
+ ' Remove-Item Env:GEMUS_KEY -ErrorAction SilentlyContinue',
21
+ ' Remove-Item Env:GEMUS_URL -ErrorAction SilentlyContinue',
22
+ '}',
23
+ ].join('\n')
24
+
25
+ const MACOS_SCRIPT = [
26
+ 'set -e',
27
+ "trap 'unset GEMUS_KEY GEMUS_URL' EXIT",
28
+ 'if [ -n "${GEMUS_URL:-}" ]; then /bin/launchctl setenv GEMUS_URL "$GEMUS_URL"; fi',
29
+ '/bin/launchctl setenv GEMUS_KEY "$GEMUS_KEY"',
30
+ ].join('\n')
31
+
32
+ function childEnvironment(key, url, parentEnv) {
33
+ const environment = Object.fromEntries(Object.entries(parentEnv).filter(([name]) => (
34
+ name.toUpperCase() !== 'GEMUS_KEY' && name.toUpperCase() !== 'GEMUS_URL'
35
+ )))
36
+ environment.GEMUS_KEY = key
37
+ if (url !== undefined) environment.GEMUS_URL = url
38
+ return environment
39
+ }
40
+
41
+ function runChild(command, args, { env }) {
42
+ return new Promise((resolve, reject) => {
43
+ const child = spawn(command, args, { env, stdio: 'ignore' })
44
+ child.once('error', reject)
45
+ child.once('exit', (code) => {
46
+ if (code === 0) resolve()
47
+ else reject(new Error('environment installer exited unsuccessfully'))
48
+ })
49
+ })
50
+ }
51
+
52
+ export async function installGemusEnvironment({
53
+ key,
54
+ url,
55
+ runner = runChild,
56
+ platform = process.platform,
57
+ stdout = process.stdout,
58
+ parentEnv = process.env,
59
+ } = {}) {
60
+ if (platform !== 'win32' && platform !== 'darwin') {
61
+ throw new Error('Gemus Companion setup supports only Windows and macOS.')
62
+ }
63
+
64
+ const env = childEnvironment(key, url, parentEnv)
65
+ try {
66
+ if (platform === 'win32') {
67
+ await runner('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', WINDOWS_SCRIPT], { env })
68
+ return
69
+ }
70
+ await runner('/bin/sh', ['-c', MACOS_SCRIPT], { env })
71
+ stdout?.write(
72
+ 'Gemus environment values last only for the current macOS login session. '
73
+ + 'After logout, reboot, or a new login, rerun setup. '
74
+ + 'Fully quit and restart Codex Desktop to use Gemus Companion.\n',
75
+ )
76
+ } catch {
77
+ throw new Error('Could not install the Gemus Companion environment.')
78
+ }
79
+ }
@@ -0,0 +1,91 @@
1
+ function present(value) {
2
+ return value !== undefined && value !== ''
3
+ }
4
+
5
+ function validateKey(key) {
6
+ if (typeof key !== 'string' || key.length === 0 || /\s/.test(key)) {
7
+ throw new Error('invalid GEMUS_KEY')
8
+ }
9
+ return key
10
+ }
11
+
12
+ function validateUrl(url) {
13
+ if (typeof url !== 'string' || url.length === 0 || /\s/.test(url)) {
14
+ throw new Error('invalid GEMUS_URL')
15
+ }
16
+ let parsed
17
+ try {
18
+ parsed = new URL(url)
19
+ } catch {
20
+ throw new Error('invalid GEMUS_URL')
21
+ }
22
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
23
+ throw new Error('invalid GEMUS_URL')
24
+ }
25
+ return url
26
+ }
27
+
28
+ function hiddenKey({ stdin, stdout }) {
29
+ stdout?.write('Enter Gemus API key: ')
30
+ return new Promise((resolve, reject) => {
31
+ let key = ''
32
+ const finish = (callback, value) => {
33
+ stdin.off('data', onData)
34
+ stdin.off('end', onEnd)
35
+ stdin.off('error', onError)
36
+ callback(value)
37
+ }
38
+ const onData = (chunk) => {
39
+ for (const character of String(chunk)) {
40
+ if (character === '\u0003') {
41
+ finish(reject, new Error('Gemus setup cancelled.'))
42
+ return
43
+ }
44
+ if (character === '\r' || character === '\n') {
45
+ if (key) finish(resolve, key)
46
+ else finish(reject, new Error('No Gemus API key was provided.'))
47
+ return
48
+ }
49
+ if (character === '\b' || character === '\u007f') {
50
+ key = key.slice(0, -1)
51
+ continue
52
+ }
53
+ key += character
54
+ }
55
+ }
56
+ const onEnd = () => finish(reject, new Error('No Gemus API key was provided.'))
57
+ const onError = () => finish(reject, new Error('Could not read the Gemus API key.'))
58
+
59
+ stdin.on('data', onData)
60
+ stdin.once('end', onEnd)
61
+ stdin.once('error', onError)
62
+ })
63
+ }
64
+
65
+ async function promptForKey({ stdin, stdout }) {
66
+ if (!stdin?.isTTY || typeof stdin.setRawMode !== 'function') {
67
+ throw new Error('GEMUS_KEY is required; set GEMUS_KEY before running setup.')
68
+ }
69
+ let rawModeEnabled = false
70
+ try {
71
+ stdin.setRawMode(true)
72
+ rawModeEnabled = true
73
+ return validateKey(await hiddenKey({ stdin, stdout }))
74
+ } finally {
75
+ if (rawModeEnabled) stdin.setRawMode(false)
76
+ }
77
+ }
78
+
79
+ export async function acquireGemusSecret({
80
+ env = process.env,
81
+ legacyKey,
82
+ legacyUrl,
83
+ stdin = process.stdin,
84
+ stdout = process.stdout,
85
+ } = {}) {
86
+ const configuredUrl = present(env.GEMUS_URL) ? env.GEMUS_URL : legacyUrl
87
+ const url = configuredUrl === undefined ? undefined : validateUrl(configuredUrl)
88
+ const configuredKey = present(env.GEMUS_KEY) ? env.GEMUS_KEY : legacyKey
89
+ const key = configuredKey === undefined ? await promptForKey({ stdin, stdout }) : validateKey(configuredKey)
90
+ return { key, url }
91
+ }