@juspay/neurolink 11.11.4 → 11.11.6
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/CHANGELOG.md +1 -5
- package/dist/browser/neurolink.min.js +384 -384
- package/dist/cli/commands/proxy.d.ts +0 -18
- package/dist/cli/commands/proxy.js +30 -473
- package/dist/cli/proxy-clients/claudeCode.d.ts +24 -0
- package/dist/cli/proxy-clients/claudeCode.js +130 -0
- package/dist/cli/proxy-clients/codex.d.ts +23 -0
- package/dist/cli/proxy-clients/codex.js +217 -0
- package/dist/cli/proxy-clients/copilot.d.ts +40 -0
- package/dist/cli/proxy-clients/copilot.js +124 -0
- package/dist/cli/proxy-clients/openCode.d.ts +26 -0
- package/dist/cli/proxy-clients/openCode.js +162 -0
- package/dist/cli/proxy-clients/qwenCode.d.ts +36 -0
- package/dist/cli/proxy-clients/qwenCode.js +139 -0
- package/dist/cli/proxy-clients/registry.d.ts +19 -0
- package/dist/cli/proxy-clients/registry.js +69 -0
- package/dist/lib/providers/googleAiStudio/client.js +96 -86
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/proxyClient.d.ts +54 -0
- package/dist/lib/types/proxyClient.js +2 -0
- package/dist/providers/googleAiStudio/client.js +96 -86
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/proxyClient.d.ts +54 -0
- package/dist/types/proxyClient.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code client configurator.
|
|
3
|
+
*
|
|
4
|
+
* Moved verbatim out of `proxy.ts`, with one behaviour change: `detect()` is
|
|
5
|
+
* new. This was the only writer that created its config file for a CLI that
|
|
6
|
+
* may never have been installed.
|
|
7
|
+
*/
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { logger } from "../../lib/utils/logger.js";
|
|
11
|
+
/**
|
|
12
|
+
* Resolved per call rather than at module load so `detect()` and `apply()`
|
|
13
|
+
* agree when HOME changes — under test, and on the `--dev` isolation path.
|
|
14
|
+
*/
|
|
15
|
+
function getClaudeSettingsDir() {
|
|
16
|
+
return join(homedir(), ".claude");
|
|
17
|
+
}
|
|
18
|
+
function getClaudeSettingsPath() {
|
|
19
|
+
return join(getClaudeSettingsDir(), "settings.json");
|
|
20
|
+
}
|
|
21
|
+
/** Keys we manage in Claude Code's settings.env */
|
|
22
|
+
const PROXY_MANAGED_KEYS = ["ANTHROPIC_BASE_URL", "ENABLE_TOOL_SEARCH"];
|
|
23
|
+
export async function setClaudeProxySettings(baseUrl) {
|
|
24
|
+
const fs = await import("fs");
|
|
25
|
+
let settings = {};
|
|
26
|
+
try {
|
|
27
|
+
settings = JSON.parse(fs.readFileSync(getClaudeSettingsPath(), "utf8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// file missing/invalid — create fresh settings object
|
|
31
|
+
}
|
|
32
|
+
const env = (settings.env ?? {});
|
|
33
|
+
// Preserve original values so clearClaudeProxySettings can restore them.
|
|
34
|
+
// Only snapshot once — subsequent calls should not overwrite the snapshot.
|
|
35
|
+
const originals = (settings
|
|
36
|
+
.__proxy_original_env ?? {});
|
|
37
|
+
for (const key of PROXY_MANAGED_KEYS) {
|
|
38
|
+
if (!(key in originals)) {
|
|
39
|
+
originals[key] = key in env ? env[key] : null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
settings.__proxy_original_env = originals;
|
|
43
|
+
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
44
|
+
env.ENABLE_TOOL_SEARCH = "true";
|
|
45
|
+
settings.env = env;
|
|
46
|
+
fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2));
|
|
47
|
+
}
|
|
48
|
+
export async function clearClaudeProxySettings(expectedBaseUrl) {
|
|
49
|
+
const fs = await import("fs");
|
|
50
|
+
let settings;
|
|
51
|
+
try {
|
|
52
|
+
settings = JSON.parse(fs.readFileSync(getClaudeSettingsPath(), "utf8"));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const env = settings.env;
|
|
58
|
+
if (!env) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (expectedBaseUrl &&
|
|
62
|
+
typeof env.ANTHROPIC_BASE_URL === "string" &&
|
|
63
|
+
env.ANTHROPIC_BASE_URL !== expectedBaseUrl) {
|
|
64
|
+
// User switched to a different proxy URL; do not clobber.
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (!("__proxy_original_env" in settings)) {
|
|
68
|
+
// No snapshot means we cannot prove these keys are ours. Without this
|
|
69
|
+
// guard the loop below reads every managed key as "did not exist before"
|
|
70
|
+
// and deletes it — wiping a real user value. Leaving a stale proxy URL
|
|
71
|
+
// behind is recoverable; destroying the user's own setting is not.
|
|
72
|
+
// OpenCode and Qwen refuse for the same reason.
|
|
73
|
+
logger.debug("[proxy] Claude clear: no original-env snapshot found, leaving settings.env intact");
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const hadBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string";
|
|
77
|
+
const hadToolSearch = env.ENABLE_TOOL_SEARCH === "true";
|
|
78
|
+
// Restore original values if they were saved, otherwise delete the keys
|
|
79
|
+
const originals = (settings
|
|
80
|
+
.__proxy_original_env ?? {});
|
|
81
|
+
for (const key of PROXY_MANAGED_KEYS) {
|
|
82
|
+
const original = originals[key];
|
|
83
|
+
if (original !== undefined && original !== null) {
|
|
84
|
+
// Restore the value that existed before the proxy was started
|
|
85
|
+
env[key] = original;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Key did not exist before — remove it
|
|
89
|
+
delete env[key];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
delete settings.__proxy_original_env;
|
|
93
|
+
if (Object.keys(env).length === 0) {
|
|
94
|
+
delete settings.env;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
settings.env = env;
|
|
98
|
+
}
|
|
99
|
+
fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2));
|
|
100
|
+
return hadBaseUrl || hadToolSearch;
|
|
101
|
+
}
|
|
102
|
+
export const claudeCodeConfigurator = {
|
|
103
|
+
id: "claude-code",
|
|
104
|
+
displayName: "Claude Code",
|
|
105
|
+
// New: previously this writer created ~/.claude/settings.json even when
|
|
106
|
+
// Claude Code had never been installed. The other two writers already
|
|
107
|
+
// probed; this one did not.
|
|
108
|
+
detect: async () => {
|
|
109
|
+
const fs = await import("fs");
|
|
110
|
+
try {
|
|
111
|
+
fs.accessSync(getClaudeSettingsDir());
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
apply: async (proxyBaseUrl) => {
|
|
119
|
+
await setClaudeProxySettings(proxyBaseUrl);
|
|
120
|
+
return true;
|
|
121
|
+
},
|
|
122
|
+
restore: (proxyBaseUrl) => clearClaudeProxySettings(proxyBaseUrl),
|
|
123
|
+
};
|
|
124
|
+
export const __claudeCodeTestHooks = {
|
|
125
|
+
getClaudeSettingsDir,
|
|
126
|
+
getClaudeSettingsPath,
|
|
127
|
+
setClaudeProxySettings,
|
|
128
|
+
clearClaudeProxySettings,
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=claudeCode.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex (ChatGPT) client configurator.
|
|
3
|
+
*
|
|
4
|
+
* Moved verbatim out of `proxy.ts`. Codex is the writer the other two should
|
|
5
|
+
* have copied: it probes for an installed CLI and reports whether it wrote.
|
|
6
|
+
*/
|
|
7
|
+
import type { CliProxyClientConfigurator } from "../../lib/types/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Resolved per call rather than at module load so `detect()` and `apply()`
|
|
10
|
+
* agree when HOME changes — under test, and on the `--dev` isolation path.
|
|
11
|
+
*/
|
|
12
|
+
declare function getCodexConfigPath(): string;
|
|
13
|
+
declare function getCodexSnapshotPath(): string;
|
|
14
|
+
export declare function setCodexProxySettings(baseUrl: string): Promise<boolean>;
|
|
15
|
+
export declare function clearCodexProxySettings(expectedBaseUrl?: string): Promise<boolean>;
|
|
16
|
+
export declare const codexConfigurator: CliProxyClientConfigurator;
|
|
17
|
+
export declare const __codexClientTestHooks: {
|
|
18
|
+
getCodexConfigPath: typeof getCodexConfigPath;
|
|
19
|
+
getCodexSnapshotPath: typeof getCodexSnapshotPath;
|
|
20
|
+
setCodexProxySettings: typeof setCodexProxySettings;
|
|
21
|
+
clearCodexProxySettings: typeof clearCodexProxySettings;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex (ChatGPT) client configurator.
|
|
3
|
+
*
|
|
4
|
+
* Moved verbatim out of `proxy.ts`. Codex is the writer the other two should
|
|
5
|
+
* have copied: it probes for an installed CLI and reports whether it wrote.
|
|
6
|
+
*/
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { logger } from "../../lib/utils/logger.js";
|
|
10
|
+
//
|
|
11
|
+
// Points the Codex CLI at the proxy by managing `~/.codex/config.toml`:
|
|
12
|
+
// - appends a marker-delimited `[model_providers.neurolink]` table
|
|
13
|
+
// - flips the top-level `model_provider` to "neurolink"
|
|
14
|
+
// The original `model_provider` value is snapshotted to a sidecar JSON so the
|
|
15
|
+
// restore works even across process crashes. All edits are guarded and wrapped
|
|
16
|
+
// so a failure never aborts proxy start/stop. Codex talks the Responses API to
|
|
17
|
+
// the proxy's /backend-api/codex path.
|
|
18
|
+
/**
|
|
19
|
+
* Resolved per call rather than at module load so `detect()` and `apply()`
|
|
20
|
+
* agree when HOME changes — under test, and on the `--dev` isolation path.
|
|
21
|
+
*/
|
|
22
|
+
function getCodexConfigPath() {
|
|
23
|
+
return join(homedir(), ".codex", "config.toml");
|
|
24
|
+
}
|
|
25
|
+
function getCodexSnapshotPath() {
|
|
26
|
+
return join(homedir(), ".neurolink", "codex-proxy-snapshot.json");
|
|
27
|
+
}
|
|
28
|
+
const CODEX_BLOCK_BEGIN = "# >>> neurolink-proxy (managed) >>>";
|
|
29
|
+
const CODEX_BLOCK_END = "# <<< neurolink-proxy (managed) <<<";
|
|
30
|
+
const CODEX_PROVIDER_LINE_RE = /^[ \t]*model_provider[ \t]*=.*$/m;
|
|
31
|
+
const CODEX_MODEL_LINE_RE = /^[ \t]*model[ \t]*=.*$/m;
|
|
32
|
+
/** Strip any previously-managed block + our injected provider line. */
|
|
33
|
+
function stripCodexManagedConfig(text) {
|
|
34
|
+
const blockRe = new RegExp(`\\n?${escapeRegExp(CODEX_BLOCK_BEGIN)}[\\s\\S]*?${escapeRegExp(CODEX_BLOCK_END)}\\n?`, "g");
|
|
35
|
+
let out = text.replace(blockRe, "\n");
|
|
36
|
+
// Remove our injected selector line (only the exact "neurolink" one).
|
|
37
|
+
out = out.replace(/^[ \t]*model_provider[ \t]*=[ \t]*"neurolink"[ \t]*$\n?/m, "");
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function escapeRegExp(value) {
|
|
41
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Apply a top-level TOML key edit, confined to the document preamble.
|
|
45
|
+
*
|
|
46
|
+
* In TOML every key after a `[table]` header belongs to that table. A
|
|
47
|
+
* document-wide regex therefore happily rewrites `model_provider` inside
|
|
48
|
+
* `[model_providers.foo]`, which sets a table key instead of the top-level
|
|
49
|
+
* selector: Codex keeps using its original provider while the command reports
|
|
50
|
+
* success. Only the text before the first header can hold top-level keys.
|
|
51
|
+
*/
|
|
52
|
+
function editTomlPreamble(text, edit) {
|
|
53
|
+
const headerMatch = /^[ \t]*\[/m.exec(text);
|
|
54
|
+
const boundary = headerMatch ? headerMatch.index : text.length;
|
|
55
|
+
const preamble = text.slice(0, boundary);
|
|
56
|
+
const edited = edit(preamble);
|
|
57
|
+
return edited === null ? text : edited + text.slice(boundary);
|
|
58
|
+
}
|
|
59
|
+
function buildCodexProviderBlock(baseUrl) {
|
|
60
|
+
return [
|
|
61
|
+
CODEX_BLOCK_BEGIN,
|
|
62
|
+
"[model_providers.neurolink]",
|
|
63
|
+
'name = "NeuroLink Proxy"',
|
|
64
|
+
`base_url = "${baseUrl}/backend-api/codex"`,
|
|
65
|
+
'wire_api = "responses"',
|
|
66
|
+
"requires_openai_auth = true",
|
|
67
|
+
CODEX_BLOCK_END,
|
|
68
|
+
"",
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
export async function setCodexProxySettings(baseUrl) {
|
|
72
|
+
try {
|
|
73
|
+
const fs = await import("fs");
|
|
74
|
+
if (!fs.existsSync(getCodexConfigPath())) {
|
|
75
|
+
// Codex not installed / never configured — skip silently.
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const original = fs.readFileSync(getCodexConfigPath(), "utf8");
|
|
79
|
+
// Snapshot the user's original selector once (survives crashes/restarts).
|
|
80
|
+
if (!fs.existsSync(getCodexSnapshotPath())) {
|
|
81
|
+
// Same preamble boundary the edit paths use. Matching document-wide would
|
|
82
|
+
// capture a `model_provider` belonging to a table — a legacy
|
|
83
|
+
// `[profiles.<name>]` block, say — and the restore would then write that
|
|
84
|
+
// profile's provider back as the top-level selector.
|
|
85
|
+
let providerMatch = null;
|
|
86
|
+
editTomlPreamble(original, (preamble) => {
|
|
87
|
+
providerMatch = preamble.match(CODEX_PROVIDER_LINE_RE);
|
|
88
|
+
return null;
|
|
89
|
+
});
|
|
90
|
+
// Ignore a stale managed selector line if present in the original.
|
|
91
|
+
const originalProviderLine = providerMatch && !/"neurolink"/.test(providerMatch[0])
|
|
92
|
+
? providerMatch[0]
|
|
93
|
+
: null;
|
|
94
|
+
fs.mkdirSync(join(homedir(), ".neurolink"), { recursive: true });
|
|
95
|
+
fs.writeFileSync(getCodexSnapshotPath(), JSON.stringify({ originalProviderLine }, null, 2), { mode: 0o600 });
|
|
96
|
+
}
|
|
97
|
+
let text = stripCodexManagedConfig(original);
|
|
98
|
+
// Set the selector: replace an existing top-level model_provider or insert
|
|
99
|
+
// one right after the top-level `model = ...` line (stays before any table).
|
|
100
|
+
let selectorPlaced = false;
|
|
101
|
+
text = editTomlPreamble(text, (preamble) => {
|
|
102
|
+
if (CODEX_PROVIDER_LINE_RE.test(preamble)) {
|
|
103
|
+
selectorPlaced = true;
|
|
104
|
+
return preamble.replace(CODEX_PROVIDER_LINE_RE, 'model_provider = "neurolink"');
|
|
105
|
+
}
|
|
106
|
+
if (CODEX_MODEL_LINE_RE.test(preamble)) {
|
|
107
|
+
selectorPlaced = true;
|
|
108
|
+
return preamble.replace(CODEX_MODEL_LINE_RE, (line) => `${line}\nmodel_provider = "neurolink"`);
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
});
|
|
112
|
+
if (!selectorPlaced) {
|
|
113
|
+
text = `model_provider = "neurolink"\n${text}`;
|
|
114
|
+
}
|
|
115
|
+
const trimmed = text.replace(/\s*$/, "\n");
|
|
116
|
+
fs.writeFileSync(getCodexConfigPath(), `${trimmed}\n${buildCodexProviderBlock(baseUrl)}`);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
logger.debug(`[proxy] Codex client config not updated: ${error instanceof Error ? error.message : String(error)}`);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export async function clearCodexProxySettings(expectedBaseUrl) {
|
|
125
|
+
try {
|
|
126
|
+
const fs = await import("fs");
|
|
127
|
+
if (!fs.existsSync(getCodexConfigPath())) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const original = fs.readFileSync(getCodexConfigPath(), "utf8");
|
|
131
|
+
// The managed block records its owner in `base_url`. Without this check a
|
|
132
|
+
// second proxy shutting down on another port would strip the block that the
|
|
133
|
+
// still-running proxy installed, and restore the snapshot selector on top —
|
|
134
|
+
// silently taking Codex off the live proxy. Mirrors the Claude and OpenCode
|
|
135
|
+
// clear paths.
|
|
136
|
+
if (expectedBaseUrl) {
|
|
137
|
+
const ownerMatch = original.match(new RegExp(`${escapeRegExp(CODEX_BLOCK_BEGIN)}[\\s\\S]*?base_url\\s*=\\s*"([^"]*)"`));
|
|
138
|
+
if (ownerMatch &&
|
|
139
|
+
ownerMatch[1] !== `${expectedBaseUrl}/backend-api/codex`) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let text = stripCodexManagedConfig(original);
|
|
144
|
+
// Restore the user's original selector line if we snapshotted one.
|
|
145
|
+
let restored = null;
|
|
146
|
+
if (fs.existsSync(getCodexSnapshotPath())) {
|
|
147
|
+
try {
|
|
148
|
+
const snap = JSON.parse(fs.readFileSync(getCodexSnapshotPath(), "utf8"));
|
|
149
|
+
restored = snap.originalProviderLine ?? null;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
restored = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (restored) {
|
|
156
|
+
const restoredLine = restored;
|
|
157
|
+
let restorePlaced = false;
|
|
158
|
+
text = editTomlPreamble(text, (preamble) => {
|
|
159
|
+
if (CODEX_PROVIDER_LINE_RE.test(preamble)) {
|
|
160
|
+
restorePlaced = true;
|
|
161
|
+
return preamble.replace(CODEX_PROVIDER_LINE_RE, restoredLine);
|
|
162
|
+
}
|
|
163
|
+
if (CODEX_MODEL_LINE_RE.test(preamble)) {
|
|
164
|
+
restorePlaced = true;
|
|
165
|
+
return preamble.replace(CODEX_MODEL_LINE_RE, (line) => `${line}\n${restoredLine}`);
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
});
|
|
169
|
+
if (!restorePlaced) {
|
|
170
|
+
text = `${restoredLine}\n${text}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (text === original) {
|
|
174
|
+
// Nothing managed remains, so the snapshot can no longer describe the
|
|
175
|
+
// user's current selector. Keeping it would let a later clear restore a
|
|
176
|
+
// value the user has since changed by hand.
|
|
177
|
+
try {
|
|
178
|
+
fs.rmSync(getCodexSnapshotPath(), { force: true });
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// best-effort
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
fs.writeFileSync(getCodexConfigPath(), text.replace(/\s*$/, "\n"));
|
|
186
|
+
try {
|
|
187
|
+
fs.rmSync(getCodexSnapshotPath(), { force: true });
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// best-effort
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
logger.debug(`[proxy] Codex client config not cleared: ${error instanceof Error ? error.message : String(error)}`);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export const codexConfigurator = {
|
|
200
|
+
id: "codex",
|
|
201
|
+
displayName: "Codex",
|
|
202
|
+
detect: async () => {
|
|
203
|
+
const fs = await import("fs");
|
|
204
|
+
return fs.existsSync(getCodexConfigPath());
|
|
205
|
+
},
|
|
206
|
+
// setCodexProxySettings appends "/backend-api/codex" itself, so this takes
|
|
207
|
+
// the bare origin.
|
|
208
|
+
apply: (proxyBaseUrl) => setCodexProxySettings(proxyBaseUrl),
|
|
209
|
+
restore: (proxyBaseUrl) => clearCodexProxySettings(proxyBaseUrl),
|
|
210
|
+
};
|
|
211
|
+
export const __codexClientTestHooks = {
|
|
212
|
+
getCodexConfigPath,
|
|
213
|
+
getCodexSnapshotPath,
|
|
214
|
+
setCodexProxySettings,
|
|
215
|
+
clearCodexProxySettings,
|
|
216
|
+
};
|
|
217
|
+
//# sourceMappingURL=codex.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot CLI client configurator.
|
|
3
|
+
*
|
|
4
|
+
* Copilot is the odd one out: its provider settings are read **only** from the
|
|
5
|
+
* environment. `app.js` resolves `COPILOT_PROVIDER_BASE_URL` (and siblings) via
|
|
6
|
+
* `process.env`, and `~/.copilot/config.json` — which announces itself as
|
|
7
|
+
* "managed automatically" — carries no provider block. There is no file this
|
|
8
|
+
* proxy can write that Copilot will read.
|
|
9
|
+
*
|
|
10
|
+
* So rather than edit a shell profile, which lives outside this proxy's blast
|
|
11
|
+
* radius and runs on every shell the user opens, `apply()` writes a small
|
|
12
|
+
* sourceable script inside the proxy's own directory. The user adds one line to
|
|
13
|
+
* their profile once:
|
|
14
|
+
*
|
|
15
|
+
* [ -f ~/.neurolink/copilot-env.sh ] && . ~/.neurolink/copilot-env.sh
|
|
16
|
+
*
|
|
17
|
+
* `restore()` deletes the script, so a sourced-but-stale file cannot outlive
|
|
18
|
+
* the proxy — the guard above makes a missing file a no-op.
|
|
19
|
+
*
|
|
20
|
+
* Copilot also honours `OPENAI_API_KEY` + `OPENAI_BASE_URL`, but only when
|
|
21
|
+
* `COPILOT_ENABLE_ALT_PROVIDERS=true` is set; the `COPILOT_PROVIDER_*` path has
|
|
22
|
+
* no such gate, so that is the one used here.
|
|
23
|
+
*/
|
|
24
|
+
import type { CliProxyClientConfigurator } from "../../lib/types/index.js";
|
|
25
|
+
/**
|
|
26
|
+
* Resolved per call rather than at module load so `detect()` and `apply()`
|
|
27
|
+
* agree when HOME changes — under test, and on the `--dev` isolation path.
|
|
28
|
+
*/
|
|
29
|
+
declare function getCopilotConfigDir(): string;
|
|
30
|
+
declare function getCopilotEnvPath(): string;
|
|
31
|
+
export declare function setCopilotProxySettings(baseUrl: string, proxyKey?: string): Promise<boolean>;
|
|
32
|
+
export declare function clearCopilotProxySettings(expectedBaseUrl?: string): Promise<boolean>;
|
|
33
|
+
export declare const copilotConfigurator: CliProxyClientConfigurator;
|
|
34
|
+
export declare const __copilotTestHooks: {
|
|
35
|
+
getCopilotConfigDir: typeof getCopilotConfigDir;
|
|
36
|
+
getCopilotEnvPath: typeof getCopilotEnvPath;
|
|
37
|
+
setCopilotProxySettings: typeof setCopilotProxySettings;
|
|
38
|
+
clearCopilotProxySettings: typeof clearCopilotProxySettings;
|
|
39
|
+
};
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot CLI client configurator.
|
|
3
|
+
*
|
|
4
|
+
* Copilot is the odd one out: its provider settings are read **only** from the
|
|
5
|
+
* environment. `app.js` resolves `COPILOT_PROVIDER_BASE_URL` (and siblings) via
|
|
6
|
+
* `process.env`, and `~/.copilot/config.json` — which announces itself as
|
|
7
|
+
* "managed automatically" — carries no provider block. There is no file this
|
|
8
|
+
* proxy can write that Copilot will read.
|
|
9
|
+
*
|
|
10
|
+
* So rather than edit a shell profile, which lives outside this proxy's blast
|
|
11
|
+
* radius and runs on every shell the user opens, `apply()` writes a small
|
|
12
|
+
* sourceable script inside the proxy's own directory. The user adds one line to
|
|
13
|
+
* their profile once:
|
|
14
|
+
*
|
|
15
|
+
* [ -f ~/.neurolink/copilot-env.sh ] && . ~/.neurolink/copilot-env.sh
|
|
16
|
+
*
|
|
17
|
+
* `restore()` deletes the script, so a sourced-but-stale file cannot outlive
|
|
18
|
+
* the proxy — the guard above makes a missing file a no-op.
|
|
19
|
+
*
|
|
20
|
+
* Copilot also honours `OPENAI_API_KEY` + `OPENAI_BASE_URL`, but only when
|
|
21
|
+
* `COPILOT_ENABLE_ALT_PROVIDERS=true` is set; the `COPILOT_PROVIDER_*` path has
|
|
22
|
+
* no such gate, so that is the one used here.
|
|
23
|
+
*/
|
|
24
|
+
import { homedir } from "os";
|
|
25
|
+
import { join } from "path";
|
|
26
|
+
import { logger } from "../../lib/utils/logger.js";
|
|
27
|
+
/**
|
|
28
|
+
* Resolved per call rather than at module load so `detect()` and `apply()`
|
|
29
|
+
* agree when HOME changes — under test, and on the `--dev` isolation path.
|
|
30
|
+
*/
|
|
31
|
+
function getCopilotConfigDir() {
|
|
32
|
+
return join(homedir(), ".copilot");
|
|
33
|
+
}
|
|
34
|
+
function getCopilotEnvPath() {
|
|
35
|
+
return join(homedir(), ".neurolink", "copilot-env.sh");
|
|
36
|
+
}
|
|
37
|
+
function buildCopilotEnvScript(baseUrl, proxyKey) {
|
|
38
|
+
return [
|
|
39
|
+
"# Generated by `neurolink proxy` — do not edit.",
|
|
40
|
+
"# Removed automatically when the proxy stops.",
|
|
41
|
+
"#",
|
|
42
|
+
"# Copilot CLI reads its provider settings from the environment only, so",
|
|
43
|
+
"# source this from your shell profile:",
|
|
44
|
+
"# [ -f ~/.neurolink/copilot-env.sh ] && . ~/.neurolink/copilot-env.sh",
|
|
45
|
+
'export COPILOT_PROVIDER_TYPE="openai"',
|
|
46
|
+
`export COPILOT_PROVIDER_BASE_URL="${baseUrl}"`,
|
|
47
|
+
`export COPILOT_PROVIDER_API_KEY="${proxyKey}"`,
|
|
48
|
+
"",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
export async function setCopilotProxySettings(baseUrl, proxyKey) {
|
|
52
|
+
const fs = await import("fs");
|
|
53
|
+
try {
|
|
54
|
+
fs.accessSync(getCopilotConfigDir());
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Copilot CLI not installed — report the skip so no caller prints a
|
|
58
|
+
// success message for work that did not happen.
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const envPath = getCopilotEnvPath();
|
|
63
|
+
fs.mkdirSync(join(homedir(), ".neurolink"), { recursive: true });
|
|
64
|
+
fs.writeFileSync(envPath, buildCopilotEnvScript(baseUrl, proxyKey || "neurolink-proxy"), { mode: 0o600 });
|
|
65
|
+
// The mode option above only applies when open() creates the file. On an
|
|
66
|
+
// overwrite it is ignored, so a pre-existing file keeps whatever
|
|
67
|
+
// permissions it had — which for a file holding a proxy key could be
|
|
68
|
+
// world-readable. Enforce it on both paths.
|
|
69
|
+
fs.chmodSync(envPath, 0o600);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger.debug(`[proxy] Copilot env script not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function clearCopilotProxySettings(expectedBaseUrl) {
|
|
78
|
+
const fs = await import("fs");
|
|
79
|
+
const envPath = getCopilotEnvPath();
|
|
80
|
+
let existing;
|
|
81
|
+
try {
|
|
82
|
+
existing = fs.readFileSync(envPath, "utf8");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// The whole file is ours to delete — but only if it is still pointing where
|
|
88
|
+
// we pointed it. A user who hand-edited it to another endpoint keeps it.
|
|
89
|
+
if (expectedBaseUrl && !existing.includes(expectedBaseUrl)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
fs.rmSync(envPath, { force: true });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
logger.debug(`[proxy] Copilot env script not removed: ${error instanceof Error ? error.message : String(error)}`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export const copilotConfigurator = {
|
|
102
|
+
id: "copilot",
|
|
103
|
+
displayName: "Copilot CLI",
|
|
104
|
+
detect: async () => {
|
|
105
|
+
const fs = await import("fs");
|
|
106
|
+
try {
|
|
107
|
+
fs.accessSync(getCopilotConfigDir());
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
// Copilot speaks OpenAI Chat Completions, so it points at the /v1 door.
|
|
115
|
+
apply: (proxyBaseUrl) => setCopilotProxySettings(`${proxyBaseUrl}/v1`),
|
|
116
|
+
restore: (proxyBaseUrl) => clearCopilotProxySettings(`${proxyBaseUrl}/v1`),
|
|
117
|
+
};
|
|
118
|
+
export const __copilotTestHooks = {
|
|
119
|
+
getCopilotConfigDir,
|
|
120
|
+
getCopilotEnvPath,
|
|
121
|
+
setCopilotProxySettings,
|
|
122
|
+
clearCopilotProxySettings,
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=copilot.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode client configurator.
|
|
3
|
+
*
|
|
4
|
+
* Moved verbatim out of `proxy.ts` so that adding a CLI means adding a file
|
|
5
|
+
* here rather than editing a 5,000-line command module in seven places.
|
|
6
|
+
*/
|
|
7
|
+
import type { CliProxyClientConfigurator } from "../../lib/types/index.js";
|
|
8
|
+
declare function getOpenCodeConfigDir(): string;
|
|
9
|
+
declare function getOpenCodeConfigPath(): string;
|
|
10
|
+
export declare function setOpenCodeProxySettings(baseUrl: string, proxyKey?: string): Promise<boolean>;
|
|
11
|
+
export declare function clearOpenCodeProxySettings(expectedBaseUrl?: string): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Test-only export (CLAUDE.md rule 15 determinism exception). The OpenCode
|
|
14
|
+
* client writers resolve paths from the environment and are only reachable
|
|
15
|
+
* from `proxy start` / `proxy setup`, neither of which can be driven against a
|
|
16
|
+
* throwaway HOME without starting a real server. Consumed by
|
|
17
|
+
* test/continuous-test-suite-proxy.ts.
|
|
18
|
+
*/
|
|
19
|
+
export declare const __openCodeTestHooks: {
|
|
20
|
+
getOpenCodeConfigDir: typeof getOpenCodeConfigDir;
|
|
21
|
+
getOpenCodeConfigPath: typeof getOpenCodeConfigPath;
|
|
22
|
+
setOpenCodeProxySettings: typeof setOpenCodeProxySettings;
|
|
23
|
+
clearOpenCodeProxySettings: typeof clearOpenCodeProxySettings;
|
|
24
|
+
};
|
|
25
|
+
export declare const openCodeConfigurator: CliProxyClientConfigurator;
|
|
26
|
+
export {};
|