@juspay/neurolink 12.30.0 โ 12.30.2
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 +3 -3
- package/dist/cli/proxy-clients/codex.js +112 -42
- package/dist/cli/proxy-clients/grok.d.ts +15 -1
- package/dist/cli/proxy-clients/grok.js +186 -28
- package/dist/cli/proxy-clients/snapshot.d.ts +12 -0
- package/dist/cli/proxy-clients/snapshot.js +37 -4
- package/dist/cli/proxy-clients/tomlSections.d.ts +74 -0
- package/dist/cli/proxy-clients/tomlSections.js +365 -0
- package/dist/types/proxyClient.d.ts +28 -3
- package/docs-site/static/search-index.json +13 -2
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
## [12.30.
|
|
1
|
+
## [12.30.2](https://github.com/juspay/neurolink/compare/v12.30.1...v12.30.2) (2026-09-26)
|
|
2
2
|
|
|
3
|
-
###
|
|
3
|
+
### Bug Fixes
|
|
4
4
|
|
|
5
|
-
- **(
|
|
5
|
+
- **(ci):** read branch protection through read-access endpoints in the audit ([19e08e2](https://github.com/juspay/neurolink/commit/19e08e214cdd699c2a22cc0de45d781a163fc473))
|
|
6
6
|
|
|
7
7
|
## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
|
|
8
8
|
|
|
@@ -8,6 +8,7 @@ import { homedir } from "os";
|
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { logger } from "../../utils/logger.js";
|
|
10
10
|
import { writeFileAtomic } from "./snapshot.js";
|
|
11
|
+
import { dottedTableIds, findDuplicateTable, ownedLineCount, readStringValue, splitTomlSections, } from "./tomlSections.js";
|
|
11
12
|
//
|
|
12
13
|
// Points the Codex CLI at the proxy by managing `~/.codex/config.toml`:
|
|
13
14
|
// - appends a marker-delimited `[model_providers.neurolink]` table
|
|
@@ -28,18 +29,67 @@ function getCodexSnapshotPath() {
|
|
|
28
29
|
}
|
|
29
30
|
const CODEX_BLOCK_BEGIN = "# >>> neurolink-proxy (managed) >>>";
|
|
30
31
|
const CODEX_BLOCK_END = "# <<< neurolink-proxy (managed) <<<";
|
|
32
|
+
const CODEX_MANAGED_COMMENT_LINES = new Set([
|
|
33
|
+
CODEX_BLOCK_BEGIN,
|
|
34
|
+
CODEX_BLOCK_END,
|
|
35
|
+
]);
|
|
36
|
+
const CODEX_PROVIDER_ID = "neurolink";
|
|
37
|
+
const CODEX_PROVIDER_NAME = "NeuroLink Proxy";
|
|
31
38
|
const CODEX_PROVIDER_LINE_RE = /^[ \t]*model_provider[ \t]*=.*$/m;
|
|
32
39
|
const CODEX_MODEL_LINE_RE = /^[ \t]*model[ \t]*=.*$/m;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const CODEX_MANAGED_SELECTOR_RE = /^[ \t]*model_provider[ \t]*=[ \t]*"neurolink"[ \t]*$\n?/m;
|
|
41
|
+
/**
|
|
42
|
+
* What of a Codex config.toml the proxy owns, found by content rather than
|
|
43
|
+
* by the block markers. Codex's own saves keep comments (measured on
|
|
44
|
+
* codex-cli 0.155.1), but a hand edit or another tool need not, and a
|
|
45
|
+
* marker-only writer then appended a second `[model_providers.neurolink]`,
|
|
46
|
+
* which Codex cannot parse. The proxy's provider is that table carrying the
|
|
47
|
+
* name the proxy writes, with any sub-tables under it. `foreign` is set when
|
|
48
|
+
* a `neurolink` provider exists that the proxy did not write: another name, a
|
|
49
|
+
* dotted-key or array definition, or sub-tables with no provider table, which
|
|
50
|
+
* the proxy never writes. Writing ours beside it would declare the table twice
|
|
51
|
+
* or adopt the user's keys. `remainder` keeps every byte the proxy does not
|
|
52
|
+
* own, including comments the user wrote after its table.
|
|
53
|
+
*/
|
|
54
|
+
function inspectCodexConfig(text) {
|
|
55
|
+
const sections = splitTomlSections(text);
|
|
56
|
+
const isProvider = (section) => section.path !== null &&
|
|
57
|
+
section.path.length >= 2 &&
|
|
58
|
+
section.path[0] === "model_providers" &&
|
|
59
|
+
section.path[1] === CODEX_PROVIDER_ID;
|
|
60
|
+
const tables = sections.filter((section) => isProvider(section) && !section.arrayTable && section.path?.length === 2);
|
|
61
|
+
const ours = (section) => readStringValue(section, "name") === CODEX_PROVIDER_NAME;
|
|
62
|
+
const foreign = tables.some((table) => !ours(table)) ||
|
|
63
|
+
(tables.length === 0 && sections.some(isProvider)) ||
|
|
64
|
+
sections.some((section) => isProvider(section) && section.arrayTable) ||
|
|
65
|
+
dottedTableIds(sections, "model_providers").includes(CODEX_PROVIDER_ID);
|
|
66
|
+
const owned = sections.map((section) => isProvider(section) &&
|
|
67
|
+
!section.arrayTable &&
|
|
68
|
+
(section.path?.length === 2 ? ours(section) : !foreign));
|
|
69
|
+
const managedBaseUrls = [
|
|
70
|
+
...new Set(sections.flatMap((section, index) => {
|
|
71
|
+
const url = owned[index]
|
|
72
|
+
? readStringValue(section, "base_url")
|
|
73
|
+
: undefined;
|
|
74
|
+
return url === undefined ? [] : [url];
|
|
75
|
+
})),
|
|
76
|
+
];
|
|
77
|
+
const remainder = sections
|
|
78
|
+
.flatMap((section, index) => owned[index]
|
|
79
|
+
? section.lines.slice(ownedLineCount(section, CODEX_MANAGED_COMMENT_LINES))
|
|
80
|
+
: section.lines)
|
|
81
|
+
.filter((line) => !CODEX_MANAGED_COMMENT_LINES.has(line.trim()))
|
|
82
|
+
.join("");
|
|
83
|
+
return { remainder, managedBaseUrls, foreign };
|
|
40
84
|
}
|
|
41
|
-
|
|
42
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Remove the selector the proxy injected. Only the preamble holds top-level
|
|
87
|
+
* keys: a profile's own `model_provider = "neurolink"` is the user's.
|
|
88
|
+
*/
|
|
89
|
+
function stripCodexSelector(text) {
|
|
90
|
+
return editTomlPreamble(text, (preamble) => CODEX_MANAGED_SELECTOR_RE.test(preamble)
|
|
91
|
+
? preamble.replace(CODEX_MANAGED_SELECTOR_RE, "")
|
|
92
|
+
: null);
|
|
43
93
|
}
|
|
44
94
|
/**
|
|
45
95
|
* Apply a top-level TOML key edit, confined to the document preamble.
|
|
@@ -60,8 +110,8 @@ function editTomlPreamble(text, edit) {
|
|
|
60
110
|
function buildCodexProviderBlock(baseUrl) {
|
|
61
111
|
return [
|
|
62
112
|
CODEX_BLOCK_BEGIN,
|
|
63
|
-
|
|
64
|
-
|
|
113
|
+
`[model_providers.${CODEX_PROVIDER_ID}]`,
|
|
114
|
+
`name = "${CODEX_PROVIDER_NAME}"`,
|
|
65
115
|
`base_url = "${baseUrl}/backend-api/codex"`,
|
|
66
116
|
'wire_api = "responses"',
|
|
67
117
|
"requires_openai_auth = true",
|
|
@@ -77,6 +127,37 @@ export async function setCodexProxySettings(baseUrl) {
|
|
|
77
127
|
return false;
|
|
78
128
|
}
|
|
79
129
|
const original = fs.readFileSync(getCodexConfigPath(), "utf8");
|
|
130
|
+
const inspected = inspectCodexConfig(original);
|
|
131
|
+
if (inspected.foreign) {
|
|
132
|
+
logger.warn("[proxy] Codex: config.toml already defines a neurolink provider the proxy did not write; leaving it untouched");
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
let text = stripCodexSelector(inspected.remainder);
|
|
136
|
+
// Set the selector: replace an existing top-level model_provider or insert
|
|
137
|
+
// one right after the top-level `model = ...` line (stays before any table).
|
|
138
|
+
let selectorPlaced = false;
|
|
139
|
+
text = editTomlPreamble(text, (preamble) => {
|
|
140
|
+
if (CODEX_PROVIDER_LINE_RE.test(preamble)) {
|
|
141
|
+
selectorPlaced = true;
|
|
142
|
+
return preamble.replace(CODEX_PROVIDER_LINE_RE, 'model_provider = "neurolink"');
|
|
143
|
+
}
|
|
144
|
+
if (CODEX_MODEL_LINE_RE.test(preamble)) {
|
|
145
|
+
selectorPlaced = true;
|
|
146
|
+
return preamble.replace(CODEX_MODEL_LINE_RE, (line) => `${line}\nmodel_provider = "neurolink"`);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
});
|
|
150
|
+
if (!selectorPlaced) {
|
|
151
|
+
text = `model_provider = "neurolink"\n${text}`;
|
|
152
|
+
}
|
|
153
|
+
const next = `${text.replace(/\s*$/, "\n")}\n${buildCodexProviderBlock(baseUrl)}`;
|
|
154
|
+
// Codex will not load a config that declares a table twice. Writing into
|
|
155
|
+
// one would only bury the user's own error under ours.
|
|
156
|
+
const duplicate = findDuplicateTable(next);
|
|
157
|
+
if (duplicate !== undefined) {
|
|
158
|
+
logger.warn(`[proxy] Codex: config.toml declares [${duplicate}] more than once; leaving it untouched`);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
80
161
|
// Snapshot the user's original selector once (survives crashes/restarts).
|
|
81
162
|
if (!fs.existsSync(getCodexSnapshotPath())) {
|
|
82
163
|
// Same preamble boundary the edit paths use. Matching document-wide would
|
|
@@ -95,26 +176,11 @@ export async function setCodexProxySettings(baseUrl) {
|
|
|
95
176
|
fs.mkdirSync(join(homedir(), ".neurolink"), { recursive: true });
|
|
96
177
|
await writeFileAtomic(getCodexSnapshotPath(), JSON.stringify({ originalProviderLine }, null, 2), 0o600);
|
|
97
178
|
}
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
text = editTomlPreamble(text, (preamble) => {
|
|
103
|
-
if (CODEX_PROVIDER_LINE_RE.test(preamble)) {
|
|
104
|
-
selectorPlaced = true;
|
|
105
|
-
return preamble.replace(CODEX_PROVIDER_LINE_RE, 'model_provider = "neurolink"');
|
|
106
|
-
}
|
|
107
|
-
if (CODEX_MODEL_LINE_RE.test(preamble)) {
|
|
108
|
-
selectorPlaced = true;
|
|
109
|
-
return preamble.replace(CODEX_MODEL_LINE_RE, (line) => `${line}\nmodel_provider = "neurolink"`);
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
});
|
|
113
|
-
if (!selectorPlaced) {
|
|
114
|
-
text = `model_provider = "neurolink"\n${text}`;
|
|
179
|
+
// Every proxy start applies. Rewriting a file that already matches only
|
|
180
|
+
// widens the window for racing Codex's own saves.
|
|
181
|
+
if (next !== original) {
|
|
182
|
+
await writeFileAtomic(getCodexConfigPath(), next);
|
|
115
183
|
}
|
|
116
|
-
const trimmed = text.replace(/\s*$/, "\n");
|
|
117
|
-
await writeFileAtomic(getCodexConfigPath(), `${trimmed}\n${buildCodexProviderBlock(baseUrl)}`);
|
|
118
184
|
return true;
|
|
119
185
|
}
|
|
120
186
|
catch (error) {
|
|
@@ -129,19 +195,23 @@ export async function clearCodexProxySettings(expectedBaseUrl) {
|
|
|
129
195
|
return false;
|
|
130
196
|
}
|
|
131
197
|
const original = fs.readFileSync(getCodexConfigPath(), "utf8");
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
198
|
+
const inspected = inspectCodexConfig(original);
|
|
199
|
+
// Apply refuses such a config, so the selector pointing at the user's own
|
|
200
|
+
// provider is theirs as well.
|
|
201
|
+
if (inspected.foreign) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
// The managed table records its owner in `base_url`. Without this check a
|
|
205
|
+
// second proxy shutting down on another port would strip the table that
|
|
206
|
+
// the still-running proxy installed, and restore the snapshot selector on
|
|
207
|
+
// top โ silently taking Codex off the live proxy. Mirrors the Claude and
|
|
208
|
+
// OpenCode clear paths.
|
|
209
|
+
if (expectedBaseUrl &&
|
|
210
|
+
inspected.managedBaseUrls.length > 0 &&
|
|
211
|
+
!inspected.managedBaseUrls.includes(`${expectedBaseUrl}/backend-api/codex`)) {
|
|
212
|
+
return false;
|
|
143
213
|
}
|
|
144
|
-
let text =
|
|
214
|
+
let text = stripCodexSelector(inspected.remainder);
|
|
145
215
|
// Restore the user's original selector line if we snapshotted one.
|
|
146
216
|
let restored = null;
|
|
147
217
|
if (fs.existsSync(getCodexSnapshotPath())) {
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
* Adaptive thinking: Grok's global `default_reasoning_effort = "xhigh"`
|
|
19
19
|
* becomes Anthropic `thinking.type = "adaptive"`. Haiku 4.5 rejects that
|
|
20
20
|
* with 400. Only Claude Opus/Sonnet 4.6 and 5.x keep reasoning enabled.
|
|
21
|
+
*
|
|
22
|
+
* Ownership is decided by content, never by the block markers alone. Grok
|
|
23
|
+
* rewrites config.toml on its own saves (`/settings`, `grok mcp add`, ...)
|
|
24
|
+
* through a serializer that drops every comment, the markers included, and
|
|
25
|
+
* splits the inline `extra_headers` into a `[model.<id>.extra_headers]`
|
|
26
|
+
* sub-table (measured on Grok Build 1.0.40). A marker-only writer then kept
|
|
27
|
+
* the unmarked tables and appended a second copy of every id on the next
|
|
28
|
+
* proxy start, and TOML rejects a table declared twice, so Grok would not
|
|
29
|
+
* start. What marks an entry as ours is the placeholder key, a credential
|
|
30
|
+
* only the proxy accepts, and that survives Grok's rewrite.
|
|
21
31
|
*/
|
|
22
32
|
import type { CliGrokProxyModelSpec, CliProxyClientApplyOptions, CliProxyClientConfigurator, ModelMapping } from "../../types/index.js";
|
|
23
33
|
declare function getGrokConfigDir(): string;
|
|
@@ -26,7 +36,11 @@ declare function getGrokSnapshotPath(): string;
|
|
|
26
36
|
declare function classifyGrokProxyModel(id: string, mapping?: ModelMapping): CliGrokProxyModelSpec;
|
|
27
37
|
declare function loadRoutedModelIds(configPath?: string): Promise<string[]>;
|
|
28
38
|
declare function catalogModelIds(configPath?: string): Promise<string[]>;
|
|
29
|
-
|
|
39
|
+
/**
|
|
40
|
+
* `userModelIds` are ids the user defines themselves. Writing the proxy's
|
|
41
|
+
* entry beside theirs would declare the table twice, so theirs wins.
|
|
42
|
+
*/
|
|
43
|
+
declare function buildGrokManagedBlock(baseUrl: string, configPath?: string, userModelIds?: ReadonlySet<string>): Promise<string>;
|
|
30
44
|
export declare function setGrokProxySettings(baseUrl: string, options?: CliProxyClientApplyOptions): Promise<boolean>;
|
|
31
45
|
export declare function clearGrokProxySettings(expectedBaseUrl?: string): Promise<boolean>;
|
|
32
46
|
export declare const grokConfigurator: CliProxyClientConfigurator;
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
* Adaptive thinking: Grok's global `default_reasoning_effort = "xhigh"`
|
|
19
19
|
* becomes Anthropic `thinking.type = "adaptive"`. Haiku 4.5 rejects that
|
|
20
20
|
* with 400. Only Claude Opus/Sonnet 4.6 and 5.x keep reasoning enabled.
|
|
21
|
+
*
|
|
22
|
+
* Ownership is decided by content, never by the block markers alone. Grok
|
|
23
|
+
* rewrites config.toml on its own saves (`/settings`, `grok mcp add`, ...)
|
|
24
|
+
* through a serializer that drops every comment, the markers included, and
|
|
25
|
+
* splits the inline `extra_headers` into a `[model.<id>.extra_headers]`
|
|
26
|
+
* sub-table (measured on Grok Build 1.0.40). A marker-only writer then kept
|
|
27
|
+
* the unmarked tables and appended a second copy of every id on the next
|
|
28
|
+
* proxy start, and TOML rejects a table declared twice, so Grok would not
|
|
29
|
+
* start. What marks an entry as ours is the placeholder key, a credential
|
|
30
|
+
* only the proxy accepts, and that survives Grok's rewrite.
|
|
21
31
|
*/
|
|
22
32
|
import { readFileSync } from "fs";
|
|
23
33
|
import { homedir } from "os";
|
|
@@ -26,9 +36,19 @@ import { logger } from "../../utils/logger.js";
|
|
|
26
36
|
import { getContextWindowSize } from "../../constants/contextWindows.js";
|
|
27
37
|
import { DEFAULT_PROXY_MODEL_IDS } from "../../constants/proxyModels.js";
|
|
28
38
|
import { defaultProxyConfigPath, parseProxyConfigString, } from "../../proxy/proxyConfig.js";
|
|
29
|
-
import { isUsableSnapshot, shouldCaptureSnapshot, writeFileAtomic, } from "./snapshot.js";
|
|
39
|
+
import { isUsableSnapshot, resolveSymlinkTarget, shouldCaptureSnapshot, writeFileAtomic, } from "./snapshot.js";
|
|
40
|
+
import { dottedTableIds, findDuplicateTable, inlineTablePairs, maskTomlStrings, maskedStringValue, ownedLineCount, parseMaskedAssignment, readStringValue, splitTomlSections, tomlCodeLines, } from "./tomlSections.js";
|
|
30
41
|
const GROK_BLOCK_BEGIN = "# >>> neurolink-proxy (managed) >>>";
|
|
31
42
|
const GROK_BLOCK_END = "# <<< neurolink-proxy (managed) <<<";
|
|
43
|
+
const GROK_BLOCK_NOTES = [
|
|
44
|
+
"# Proxy catalog. Built-in grok-4.6 / grok-4.5 stay on xAI.",
|
|
45
|
+
"# context_window is Grok's compaction limit and must be <= upstream.",
|
|
46
|
+
];
|
|
47
|
+
const GROK_MANAGED_COMMENT_LINES = new Set([
|
|
48
|
+
GROK_BLOCK_BEGIN,
|
|
49
|
+
GROK_BLOCK_END,
|
|
50
|
+
...GROK_BLOCK_NOTES,
|
|
51
|
+
]);
|
|
32
52
|
const PLACEHOLDER_KEY = "neurolink-proxy";
|
|
33
53
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
34
54
|
function getGrokConfigDir() {
|
|
@@ -43,9 +63,6 @@ function getGrokConfigPath() {
|
|
|
43
63
|
function getGrokSnapshotPath() {
|
|
44
64
|
return join(homedir(), ".neurolink", "grok-proxy-snapshot.json");
|
|
45
65
|
}
|
|
46
|
-
function escapeRegExp(value) {
|
|
47
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
48
|
-
}
|
|
49
66
|
function tomlKey(id) {
|
|
50
67
|
return /^[A-Za-z0-9_-]+$/.test(id) ? id : JSON.stringify(id);
|
|
51
68
|
}
|
|
@@ -172,28 +189,148 @@ function buildGrokModelBlock(spec, baseUrl) {
|
|
|
172
189
|
}
|
|
173
190
|
return lines.join("\n");
|
|
174
191
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
/**
|
|
193
|
+
* `userModelIds` are ids the user defines themselves. Writing the proxy's
|
|
194
|
+
* entry beside theirs would declare the table twice, so theirs wins.
|
|
195
|
+
*/
|
|
196
|
+
async function buildGrokManagedBlock(baseUrl, configPath, userModelIds = new Set()) {
|
|
197
|
+
const catalog = await catalogGrokSpecs(configPath);
|
|
198
|
+
for (const spec of catalog) {
|
|
199
|
+
if (userModelIds.has(spec.id)) {
|
|
200
|
+
logger.debug(`[proxy] Grok: config.toml already defines [model.${tomlKey(spec.id)}]; keeping it and skipping the proxy's entry`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const body = catalog
|
|
204
|
+
.filter((spec) => !userModelIds.has(spec.id))
|
|
178
205
|
.map((spec) => buildGrokModelBlock(spec, baseUrl))
|
|
179
206
|
.join("\n\n");
|
|
180
207
|
return [
|
|
181
208
|
GROK_BLOCK_BEGIN,
|
|
182
|
-
|
|
183
|
-
"# context_window is Grok's compaction limit and must be <= upstream.",
|
|
209
|
+
...GROK_BLOCK_NOTES,
|
|
184
210
|
"",
|
|
185
211
|
body,
|
|
186
212
|
GROK_BLOCK_END,
|
|
187
213
|
"",
|
|
188
214
|
].join("\n");
|
|
189
215
|
}
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
216
|
+
function modelEntryId(section) {
|
|
217
|
+
return section.path !== null &&
|
|
218
|
+
section.path.length >= 2 &&
|
|
219
|
+
section.path[0] === "model"
|
|
220
|
+
? section.path[1]
|
|
221
|
+
: null;
|
|
222
|
+
}
|
|
223
|
+
/** Where a model entry carries the placeholder key, as a path within it. */
|
|
224
|
+
const PLACEHOLDER_KEY_PATHS = [
|
|
225
|
+
["api_key"],
|
|
226
|
+
["extra_headers", "x-api-key"],
|
|
227
|
+
];
|
|
228
|
+
function isPlaceholderKey(path, value) {
|
|
229
|
+
return (value === PLACEHOLDER_KEY &&
|
|
230
|
+
PLACEHOLDER_KEY_PATHS.some((keyPath) => keyPath.length === path.length &&
|
|
231
|
+
keyPath.every((part, index) => path[index] === part)));
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Whether a section of a model entry sends the placeholder key: `api_key` on
|
|
235
|
+
* the model, or `x-api-key` in its `extra_headers`, spelled as a plain key, a
|
|
236
|
+
* dotted key, an inline table or the sub-table Grok's own save writes. The
|
|
237
|
+
* full key path must match, so a user's `legacy.api_key` is not the proxy's.
|
|
238
|
+
*/
|
|
239
|
+
function sectionSendsPlaceholderKey(section) {
|
|
240
|
+
const prefix = section.path?.slice(2) ?? [];
|
|
241
|
+
return tomlCodeLines(section).some(({ line, statement }) => {
|
|
242
|
+
if (!statement) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const { masked, strings } = maskTomlStrings(line);
|
|
246
|
+
const assignment = parseMaskedAssignment(masked, strings);
|
|
247
|
+
if (!assignment) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const path = [...prefix, ...assignment.path];
|
|
251
|
+
return (isPlaceholderKey(path, maskedStringValue(assignment.value, strings)) ||
|
|
252
|
+
inlineTablePairs(assignment.value, strings).some((pair) => isPlaceholderKey([...path, ...pair.path], maskedStringValue(pair.value, strings))));
|
|
253
|
+
});
|
|
193
254
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Which parts of a Grok config.toml the proxy owns.
|
|
257
|
+
*
|
|
258
|
+
* A model entry is a `[model.<id>]` table plus its `[model.<id>.*]`
|
|
259
|
+
* sub-tables, wherever they sit: Grok's save keeps them together, hand edits
|
|
260
|
+
* need not. It is the proxy's when it sends the placeholder key and is either
|
|
261
|
+
* in the current catalog or still carries the exact name the proxy gave it,
|
|
262
|
+
* which covers entries a later catalog dropped. A user's own model behind the
|
|
263
|
+
* proxy, under their own name, is theirs. Keys and values are read as TOML,
|
|
264
|
+
* never as raw text, so a string that merely mentions the placeholder cannot
|
|
265
|
+
* make a user's table look like the proxy's. `remainder` is the text with
|
|
266
|
+
* owned entries and the managed comment lines removed and every other byte
|
|
267
|
+
* kept, so a missing, orphaned or doubled marker cannot make an apply delete
|
|
268
|
+
* the user's content or leave an old copy of the catalog behind.
|
|
269
|
+
*/
|
|
270
|
+
function inspectGrokConfig(text, catalogIds) {
|
|
271
|
+
const sections = splitTomlSections(text);
|
|
272
|
+
// Models the user defines without a [model.<id>] table of their own.
|
|
273
|
+
const definedElsewhere = new Set([
|
|
274
|
+
...dottedTableIds(sections, "model"),
|
|
275
|
+
...sections.flatMap((section) => {
|
|
276
|
+
const id = section.arrayTable ? modelEntryId(section) : null;
|
|
277
|
+
return id !== null && section.path?.length === 2 ? [id] : [];
|
|
278
|
+
}),
|
|
279
|
+
]);
|
|
280
|
+
const ids = sections.map((section) => section.arrayTable ? null : modelEntryId(section));
|
|
281
|
+
const isModelTable = (index) => ids[index] !== null && sections[index].path?.length === 2;
|
|
282
|
+
const tableFor = (index) => {
|
|
283
|
+
if (ids[index] === null || isModelTable(index)) {
|
|
284
|
+
return index;
|
|
285
|
+
}
|
|
286
|
+
const sameModel = (candidate) => isModelTable(candidate) && ids[candidate] === ids[index];
|
|
287
|
+
const before = sections.findLastIndex((_, candidate) => candidate < index && sameModel(candidate));
|
|
288
|
+
const after = sections.findIndex((_, candidate) => candidate > index && sameModel(candidate));
|
|
289
|
+
return before !== -1 ? before : after !== -1 ? after : index;
|
|
290
|
+
};
|
|
291
|
+
const entryOf = sections.map((_, index) => tableFor(index));
|
|
292
|
+
const ownedEntries = new Set(entryOf.filter((entry) => {
|
|
293
|
+
const id = ids[entry];
|
|
294
|
+
if (id === null) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const sendsPlaceholder = sections.some((section, index) => entryOf[index] === entry &&
|
|
298
|
+
ids[index] !== null &&
|
|
299
|
+
sectionSendsPlaceholderKey(section));
|
|
300
|
+
if (!sendsPlaceholder) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
// A sub-table left without its table carries no name to check; it is
|
|
304
|
+
// the proxy's unless the model is defined some other way.
|
|
305
|
+
if (!isModelTable(entry)) {
|
|
306
|
+
return !definedElsewhere.has(id);
|
|
307
|
+
}
|
|
308
|
+
return (catalogIds.has(id) ||
|
|
309
|
+
readStringValue(sections[entry], "name") === displayNameFor(id));
|
|
310
|
+
}));
|
|
311
|
+
const owned = sections.map((_, index) => ids[index] !== null && ownedEntries.has(entryOf[index]));
|
|
312
|
+
const userModelIds = new Set([
|
|
313
|
+
...definedElsewhere,
|
|
314
|
+
...sections.flatMap((section, index) => {
|
|
315
|
+
const id = modelEntryId(section);
|
|
316
|
+
return id !== null && !owned[index] ? [id] : [];
|
|
317
|
+
}),
|
|
318
|
+
]);
|
|
319
|
+
const managedBaseUrls = [
|
|
320
|
+
...new Set(sections.flatMap((section, index) => {
|
|
321
|
+
const url = owned[index]
|
|
322
|
+
? readStringValue(section, "base_url")
|
|
323
|
+
: undefined;
|
|
324
|
+
return url === undefined ? [] : [url];
|
|
325
|
+
})),
|
|
326
|
+
];
|
|
327
|
+
const remainder = sections
|
|
328
|
+
.flatMap((section, index) => owned[index]
|
|
329
|
+
? section.lines.slice(ownedLineCount(section, GROK_MANAGED_COMMENT_LINES))
|
|
330
|
+
: section.lines)
|
|
331
|
+
.filter((line) => !GROK_MANAGED_COMMENT_LINES.has(line.trim()))
|
|
332
|
+
.join("");
|
|
333
|
+
return { remainder, managedBaseUrls, userModelIds };
|
|
197
334
|
}
|
|
198
335
|
async function readGrokSnapshot() {
|
|
199
336
|
const fs = await import("fs");
|
|
@@ -250,11 +387,26 @@ export async function setGrokProxySettings(baseUrl, options) {
|
|
|
250
387
|
: original.indexOf(GROK_BLOCK_END) + GROK_BLOCK_END.length)
|
|
251
388
|
: undefined
|
|
252
389
|
: undefined;
|
|
390
|
+
const inspected = original === null
|
|
391
|
+
? null
|
|
392
|
+
: inspectGrokConfig(original, new Set(await catalogModelIds(options?.configPath)));
|
|
393
|
+
const block = await buildGrokManagedBlock(baseUrl, options?.configPath, inspected?.userModelIds);
|
|
394
|
+
const remainder = inspected?.remainder ?? "";
|
|
395
|
+
const next = remainder.trim().length === 0
|
|
396
|
+
? block
|
|
397
|
+
: `${remainder.replace(/\s*$/, "\n")}\n${block}`;
|
|
398
|
+
// Grok refuses to load or save a config it cannot parse. Writing into one
|
|
399
|
+
// would only bury the user's own error under ours.
|
|
400
|
+
const duplicate = findDuplicateTable(next);
|
|
401
|
+
if (duplicate !== undefined) {
|
|
402
|
+
logger.warn(`[proxy] Grok: config.toml declares [${duplicate}] more than once; leaving it untouched`);
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
253
405
|
if (existingSnapshot === null ||
|
|
254
406
|
shouldCaptureSnapshot({
|
|
255
407
|
hasSnapshot: existingSnapshot !== null,
|
|
256
408
|
written: existingSnapshot?.writtenBaseUrl,
|
|
257
|
-
current:
|
|
409
|
+
current: inspected?.managedBaseUrls[0] ?? currentBlock,
|
|
258
410
|
})) {
|
|
259
411
|
fs.mkdirSync(join(homedir(), ".neurolink"), { recursive: true });
|
|
260
412
|
await writeFileAtomic(getGrokSnapshotPath(), JSON.stringify({
|
|
@@ -262,12 +414,11 @@ export async function setGrokProxySettings(baseUrl, options) {
|
|
|
262
414
|
writtenBaseUrl: baseUrl,
|
|
263
415
|
}, null, 2), 0o600);
|
|
264
416
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
: next, original === null ? 0o600 : undefined);
|
|
417
|
+
// Every proxy start applies; an update starts one. Rewriting a file that
|
|
418
|
+
// already matches only widens the window for racing Grok's own saves.
|
|
419
|
+
if (next !== original) {
|
|
420
|
+
await writeFileAtomic(getGrokConfigPath(), next, original === null ? 0o600 : undefined);
|
|
421
|
+
}
|
|
271
422
|
return true;
|
|
272
423
|
}
|
|
273
424
|
export async function clearGrokProxySettings(expectedBaseUrl) {
|
|
@@ -279,13 +430,16 @@ export async function clearGrokProxySettings(expectedBaseUrl) {
|
|
|
279
430
|
catch {
|
|
280
431
|
return false;
|
|
281
432
|
}
|
|
282
|
-
|
|
433
|
+
const inspected = inspectGrokConfig(current, new Set(await catalogModelIds()));
|
|
434
|
+
if (inspected.remainder === current) {
|
|
283
435
|
return false;
|
|
284
436
|
}
|
|
285
|
-
|
|
437
|
+
// Another proxy's block is left alone. One entry the user repointed does
|
|
438
|
+
// not make the rest foreign, whichever entry happens to come first.
|
|
439
|
+
const configuredUrls = inspected.managedBaseUrls;
|
|
286
440
|
if (expectedBaseUrl &&
|
|
287
|
-
|
|
288
|
-
|
|
441
|
+
configuredUrls.length > 0 &&
|
|
442
|
+
!configuredUrls.includes(expectedBaseUrl)) {
|
|
289
443
|
logger.debug("[proxy] Grok clear: base URL is not the one we wrote, leaving it intact");
|
|
290
444
|
return false;
|
|
291
445
|
}
|
|
@@ -294,9 +448,13 @@ export async function clearGrokProxySettings(expectedBaseUrl) {
|
|
|
294
448
|
logger.warn("[proxy] Grok clear: no usable snapshot, leaving config.toml untouched rather than stripping a block we cannot prove we own");
|
|
295
449
|
return false;
|
|
296
450
|
}
|
|
297
|
-
const remainder =
|
|
451
|
+
const remainder = inspected.remainder.replace(/\s*$/, "\n");
|
|
298
452
|
if (!snapshot.originalExisted && remainder.trim().length === 0) {
|
|
299
|
-
|
|
453
|
+
// Apply wrote through a symlink the user already had, so the link stays
|
|
454
|
+
// and the file apply created behind it goes.
|
|
455
|
+
fs.rmSync(await resolveSymlinkTarget(getGrokConfigPath()), {
|
|
456
|
+
force: true,
|
|
457
|
+
});
|
|
300
458
|
}
|
|
301
459
|
else {
|
|
302
460
|
await writeFileAtomic(getGrokConfigPath(), remainder);
|
|
@@ -63,6 +63,11 @@ export declare function isProxyOwnedValue(args: {
|
|
|
63
63
|
written: unknown;
|
|
64
64
|
current: unknown;
|
|
65
65
|
}): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The path a chain of symlinks finally names, which need not exist. A path
|
|
68
|
+
* that is not a link comes back unchanged.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveSymlinkTarget(filePath: string): Promise<string>;
|
|
66
71
|
/**
|
|
67
72
|
* Replace a file's contents without ever exposing a partial one.
|
|
68
73
|
*
|
|
@@ -112,5 +117,12 @@ export declare function isProxyOwnedValue(args: {
|
|
|
112
117
|
* is NOT atomic. Moving the temp file to `os.tmpdir()` would look like a
|
|
113
118
|
* tidy-up and would quietly restore the exact window this function exists to
|
|
114
119
|
* close, on Windows only, where nobody here would see it.
|
|
120
|
+
*
|
|
121
|
+
* A config kept in a dotfiles repository is often a symlink into it, a setup
|
|
122
|
+
* Grok documents and its own writer honours. A rename over the link would
|
|
123
|
+
* replace the link with a regular file and leave the repository's copy stale,
|
|
124
|
+
* so the write lands on the end of the link chain instead, which need not
|
|
125
|
+
* exist yet. The temp file is that target's sibling, so the rename stays
|
|
126
|
+
* atomic.
|
|
115
127
|
*/
|
|
116
128
|
export declare function writeFileAtomic(filePath: string, contents: string, mode?: number): Promise<void>;
|
|
@@ -113,6 +113,31 @@ export function isProxyOwnedValue(args) {
|
|
|
113
113
|
}
|
|
114
114
|
return valuesMatch(args.current, args.written);
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* The path a chain of symlinks finally names, which need not exist. A path
|
|
118
|
+
* that is not a link comes back unchanged.
|
|
119
|
+
*/
|
|
120
|
+
export async function resolveSymlinkTarget(filePath) {
|
|
121
|
+
const fs = await import("fs");
|
|
122
|
+
const { dirname, resolve } = await import("path");
|
|
123
|
+
let target = filePath;
|
|
124
|
+
for (let hops = 0;; hops += 1) {
|
|
125
|
+
let isLink;
|
|
126
|
+
try {
|
|
127
|
+
isLink = fs.lstatSync(target).isSymbolicLink();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
isLink = false;
|
|
131
|
+
}
|
|
132
|
+
if (!isLink) {
|
|
133
|
+
return target;
|
|
134
|
+
}
|
|
135
|
+
if (hops === 40) {
|
|
136
|
+
throw new Error(`${filePath}: too many levels of symbolic links`);
|
|
137
|
+
}
|
|
138
|
+
target = resolve(dirname(target), fs.readlinkSync(target));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
116
141
|
/** Distinguishes concurrent writers within one process. */
|
|
117
142
|
let atomicWriteCounter = 0;
|
|
118
143
|
/**
|
|
@@ -164,12 +189,20 @@ let atomicWriteCounter = 0;
|
|
|
164
189
|
* is NOT atomic. Moving the temp file to `os.tmpdir()` would look like a
|
|
165
190
|
* tidy-up and would quietly restore the exact window this function exists to
|
|
166
191
|
* close, on Windows only, where nobody here would see it.
|
|
192
|
+
*
|
|
193
|
+
* A config kept in a dotfiles repository is often a symlink into it, a setup
|
|
194
|
+
* Grok documents and its own writer honours. A rename over the link would
|
|
195
|
+
* replace the link with a regular file and leave the repository's copy stale,
|
|
196
|
+
* so the write lands on the end of the link chain instead, which need not
|
|
197
|
+
* exist yet. The temp file is that target's sibling, so the rename stays
|
|
198
|
+
* atomic.
|
|
167
199
|
*/
|
|
168
200
|
export async function writeFileAtomic(filePath, contents, mode) {
|
|
169
201
|
const fs = await import("fs");
|
|
170
202
|
const { dirname, join, basename } = await import("path");
|
|
203
|
+
const target = await resolveSymlinkTarget(filePath);
|
|
171
204
|
atomicWriteCounter += 1;
|
|
172
|
-
const tempPath = join(dirname(
|
|
205
|
+
const tempPath = join(dirname(target), `.${basename(target)}.neurolink-${process.pid}-${atomicWriteCounter}.tmp`);
|
|
173
206
|
// Which step failed changes what the user should do about it: a failed write
|
|
174
207
|
// is usually a missing directory or a full disk and the config is untouched,
|
|
175
208
|
// while a failed rename is a locked destination and the config is intact but
|
|
@@ -180,7 +213,7 @@ export async function writeFileAtomic(filePath, contents, mode) {
|
|
|
180
213
|
let effectiveMode = mode;
|
|
181
214
|
if (effectiveMode === undefined) {
|
|
182
215
|
try {
|
|
183
|
-
effectiveMode = fs.statSync(
|
|
216
|
+
effectiveMode = fs.statSync(target).mode & 0o777;
|
|
184
217
|
}
|
|
185
218
|
catch {
|
|
186
219
|
effectiveMode = 0o600;
|
|
@@ -192,12 +225,12 @@ export async function writeFileAtomic(filePath, contents, mode) {
|
|
|
192
225
|
// caller sees an ENOENT naming a path it never asked to write. Creating
|
|
193
226
|
// the directory first makes a first-run write behave like the plain
|
|
194
227
|
// writeFileSync it replaced.
|
|
195
|
-
fs.mkdirSync(dirname(
|
|
228
|
+
fs.mkdirSync(dirname(target), { recursive: true });
|
|
196
229
|
fs.writeFileSync(tempPath, contents, { mode: effectiveMode });
|
|
197
230
|
stage = "chmod";
|
|
198
231
|
fs.chmodSync(tempPath, effectiveMode);
|
|
199
232
|
stage = "rename";
|
|
200
|
-
fs.renameSync(tempPath,
|
|
233
|
+
fs.renameSync(tempPath, target);
|
|
201
234
|
}
|
|
202
235
|
catch (error) {
|
|
203
236
|
// Never leave scratch in the user's config directory.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-level TOML reading for the proxy's client-config writers.
|
|
3
|
+
*
|
|
4
|
+
* Grok and Codex keep their settings in TOML files the user and the CLI
|
|
5
|
+
* itself also edit, and the writers must find what the proxy wrote there
|
|
6
|
+
* without trusting its comment markers: Grok's own save drops every comment.
|
|
7
|
+
* No TOML parser is a dependency, and a parse-and-serialize round trip would
|
|
8
|
+
* reformat the user's file anyway, so this reads just enough structure to
|
|
9
|
+
* edit text in place: table headers, strings, keys and assignments. Every
|
|
10
|
+
* line keeps its own terminator, so text a writer does not own round-trips
|
|
11
|
+
* byte for byte.
|
|
12
|
+
*/
|
|
13
|
+
import type { CliTomlSection } from "../../types/index.js";
|
|
14
|
+
export declare function splitTomlSections(text: string): CliTomlSection[];
|
|
15
|
+
/**
|
|
16
|
+
* A line of TOML with every string replaced by `@<n>@` and any comment
|
|
17
|
+
* dropped, so text inside a string can never read as a key or an assignment.
|
|
18
|
+
* `strings[n]` is the decoded value. `@` cannot occur outside a string in
|
|
19
|
+
* valid TOML.
|
|
20
|
+
*/
|
|
21
|
+
export declare function maskTomlStrings(line: string): {
|
|
22
|
+
masked: string;
|
|
23
|
+
strings: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare function parseMaskedAssignment(masked: string, strings: readonly string[]): {
|
|
26
|
+
path: string[];
|
|
27
|
+
value: string;
|
|
28
|
+
} | null;
|
|
29
|
+
/**
|
|
30
|
+
* The string a masked value starts with. Only the start counts, so a value
|
|
31
|
+
* with damage after it (a literal `\n` left by a bad paste) is still read.
|
|
32
|
+
*/
|
|
33
|
+
export declare function maskedStringValue(value: string, strings: readonly string[]): string | undefined;
|
|
34
|
+
/** The pairs directly inside a one-line inline table. */
|
|
35
|
+
export declare function inlineTablePairs(value: string, strings: readonly string[]): {
|
|
36
|
+
path: string[];
|
|
37
|
+
value: string;
|
|
38
|
+
}[];
|
|
39
|
+
/** The dotted key a statement assigns, and its value when that is a string. */
|
|
40
|
+
export declare function readTomlAssignment(line: string): {
|
|
41
|
+
path: string[];
|
|
42
|
+
value: string | undefined;
|
|
43
|
+
} | null;
|
|
44
|
+
/**
|
|
45
|
+
* The lines of a section that are TOML rather than the inside of a
|
|
46
|
+
* multi-line string; `statement` marks those that start a key/value pair
|
|
47
|
+
* rather than continue an array or inline table.
|
|
48
|
+
*/
|
|
49
|
+
export declare function tomlCodeLines(section: CliTomlSection): {
|
|
50
|
+
line: string;
|
|
51
|
+
statement: boolean;
|
|
52
|
+
}[];
|
|
53
|
+
export declare function readStringValue(section: CliTomlSection, key: string): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Names of the `<parent>.<name>` tables defined by dotted keys instead of a
|
|
56
|
+
* `[<parent>.<name>]` header: `claude-opus-4-6.base_url = ...` under
|
|
57
|
+
* `[model]`, or `model.claude-opus-4-6.base_url = ...` before the first
|
|
58
|
+
* header.
|
|
59
|
+
*/
|
|
60
|
+
export declare function dottedTableIds(sections: readonly CliTomlSection[], parent: string): string[];
|
|
61
|
+
/**
|
|
62
|
+
* How many of an owned section's lines are the writer's. Comments after its
|
|
63
|
+
* last key belong to what follows, so a note the user wrote above their own
|
|
64
|
+
* next table survives. `managedLines` are the writer's own comment lines,
|
|
65
|
+
* which are dropped wherever they sit.
|
|
66
|
+
*/
|
|
67
|
+
export declare function ownedLineCount(section: CliTomlSection, managedLines: ReadonlySet<string>): number;
|
|
68
|
+
/**
|
|
69
|
+
* A table declared twice is a TOML error, and neither Grok nor Codex will
|
|
70
|
+
* start on one.
|
|
71
|
+
* Each `[[array]]` element opens a fresh scope for everything under it, so
|
|
72
|
+
* `[mcp_servers.env]` under two elements is not a repeat.
|
|
73
|
+
*/
|
|
74
|
+
export declare function findDuplicateTable(text: string): string | undefined;
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-level TOML reading for the proxy's client-config writers.
|
|
3
|
+
*
|
|
4
|
+
* Grok and Codex keep their settings in TOML files the user and the CLI
|
|
5
|
+
* itself also edit, and the writers must find what the proxy wrote there
|
|
6
|
+
* without trusting its comment markers: Grok's own save drops every comment.
|
|
7
|
+
* No TOML parser is a dependency, and a parse-and-serialize round trip would
|
|
8
|
+
* reformat the user's file anyway, so this reads just enough structure to
|
|
9
|
+
* edit text in place: table headers, strings, keys and assignments. Every
|
|
10
|
+
* line keeps its own terminator, so text a writer does not own round-trips
|
|
11
|
+
* byte for byte.
|
|
12
|
+
*/
|
|
13
|
+
function splitLinesKeepingEnds(text) {
|
|
14
|
+
return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
15
|
+
}
|
|
16
|
+
function readTomlKey(text) {
|
|
17
|
+
const bare = text.match(/^[A-Za-z0-9_-]+/);
|
|
18
|
+
if (bare) {
|
|
19
|
+
return { value: bare[0], length: bare[0].length };
|
|
20
|
+
}
|
|
21
|
+
const basic = text.match(/^"(?:[^"\\]|\\.)*"/);
|
|
22
|
+
if (basic) {
|
|
23
|
+
let value;
|
|
24
|
+
try {
|
|
25
|
+
value = JSON.parse(basic[0]);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
value = undefined;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
value: typeof value === "string" ? value : basic[0].slice(1, -1),
|
|
32
|
+
length: basic[0].length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const literal = text.match(/^'[^']*'/);
|
|
36
|
+
if (literal) {
|
|
37
|
+
return { value: literal[0].slice(1, -1), length: literal[0].length };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function parseTomlTableHeader(line) {
|
|
42
|
+
const text = line.trim();
|
|
43
|
+
if (!text.startsWith("[")) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const arrayTable = text.startsWith("[[");
|
|
47
|
+
const path = [];
|
|
48
|
+
let rest = text.slice(arrayTable ? 2 : 1);
|
|
49
|
+
for (;;) {
|
|
50
|
+
rest = rest.trimStart();
|
|
51
|
+
const key = readTomlKey(rest);
|
|
52
|
+
if (key === null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
path.push(key.value);
|
|
56
|
+
rest = rest.slice(key.length).trimStart();
|
|
57
|
+
if (!rest.startsWith(".")) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
rest = rest.slice(1);
|
|
61
|
+
}
|
|
62
|
+
const close = arrayTable ? "]]" : "]";
|
|
63
|
+
if (!rest.startsWith(close)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const tail = rest.slice(close.length).trim();
|
|
67
|
+
return tail.length === 0 || tail.startsWith("#")
|
|
68
|
+
? { path, arrayTable }
|
|
69
|
+
: null;
|
|
70
|
+
}
|
|
71
|
+
function advanceTomlScan(line, state) {
|
|
72
|
+
let { multiline, depth } = state;
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < line.length) {
|
|
75
|
+
if (multiline !== null) {
|
|
76
|
+
if (multiline === '"""' && line[i] === "\\") {
|
|
77
|
+
i += 2;
|
|
78
|
+
}
|
|
79
|
+
else if (line.startsWith(multiline, i)) {
|
|
80
|
+
// Up to two quotes of content may sit against the closing delimiter.
|
|
81
|
+
const quote = multiline[0];
|
|
82
|
+
i += 3;
|
|
83
|
+
while (line[i] === quote) {
|
|
84
|
+
i += 1;
|
|
85
|
+
}
|
|
86
|
+
multiline = null;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
i += 1;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const ch = line[i];
|
|
94
|
+
if (ch === "#") {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
|
|
98
|
+
multiline = line.startsWith('"""', i) ? '"""' : "'''";
|
|
99
|
+
i += 3;
|
|
100
|
+
}
|
|
101
|
+
else if (ch === '"') {
|
|
102
|
+
i += 1;
|
|
103
|
+
while (i < line.length && line[i] !== '"') {
|
|
104
|
+
i += line[i] === "\\" ? 2 : 1;
|
|
105
|
+
}
|
|
106
|
+
i += 1;
|
|
107
|
+
}
|
|
108
|
+
else if (ch === "'") {
|
|
109
|
+
const end = line.indexOf("'", i + 1);
|
|
110
|
+
i = end === -1 ? line.length : end + 1;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
if (ch === "[" || ch === "{") {
|
|
114
|
+
depth += 1;
|
|
115
|
+
}
|
|
116
|
+
else if (ch === "]" || ch === "}") {
|
|
117
|
+
depth = Math.max(0, depth - 1);
|
|
118
|
+
}
|
|
119
|
+
i += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { multiline, depth };
|
|
123
|
+
}
|
|
124
|
+
export function splitTomlSections(text) {
|
|
125
|
+
const sections = [];
|
|
126
|
+
let current = { path: null, arrayTable: false, lines: [] };
|
|
127
|
+
let state = { multiline: null, depth: 0 };
|
|
128
|
+
for (const line of splitLinesKeepingEnds(text)) {
|
|
129
|
+
const header = state.multiline === null && state.depth === 0
|
|
130
|
+
? parseTomlTableHeader(line)
|
|
131
|
+
: null;
|
|
132
|
+
if (header) {
|
|
133
|
+
sections.push(current);
|
|
134
|
+
current = { ...header, lines: [line] };
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
current.lines.push(line);
|
|
138
|
+
state = advanceTomlScan(line, state);
|
|
139
|
+
}
|
|
140
|
+
sections.push(current);
|
|
141
|
+
return sections;
|
|
142
|
+
}
|
|
143
|
+
function decodeBasicString(inner) {
|
|
144
|
+
try {
|
|
145
|
+
const value = JSON.parse(`"${inner}"`);
|
|
146
|
+
return typeof value === "string" ? value : inner;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return inner;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A line of TOML with every string replaced by `@<n>@` and any comment
|
|
154
|
+
* dropped, so text inside a string can never read as a key or an assignment.
|
|
155
|
+
* `strings[n]` is the decoded value. `@` cannot occur outside a string in
|
|
156
|
+
* valid TOML.
|
|
157
|
+
*/
|
|
158
|
+
export function maskTomlStrings(line) {
|
|
159
|
+
const strings = [];
|
|
160
|
+
let masked = "";
|
|
161
|
+
let i = 0;
|
|
162
|
+
while (i < line.length) {
|
|
163
|
+
const ch = line[i];
|
|
164
|
+
if (ch === "#") {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
const triple = line.startsWith('"""', i)
|
|
168
|
+
? '"""'
|
|
169
|
+
: line.startsWith("'''", i)
|
|
170
|
+
? "'''"
|
|
171
|
+
: null;
|
|
172
|
+
if (triple !== null || ch === '"' || ch === "'") {
|
|
173
|
+
const delimiter = triple ?? ch;
|
|
174
|
+
let end = i + delimiter.length;
|
|
175
|
+
while (end < line.length && !line.startsWith(delimiter, end)) {
|
|
176
|
+
end += delimiter.startsWith('"') && line[end] === "\\" ? 2 : 1;
|
|
177
|
+
}
|
|
178
|
+
// Up to two quotes of content may sit against a triple closer.
|
|
179
|
+
for (let extra = 0; triple !== null &&
|
|
180
|
+
extra < 2 &&
|
|
181
|
+
line[end + delimiter.length] === delimiter[0]; extra += 1) {
|
|
182
|
+
end += 1;
|
|
183
|
+
}
|
|
184
|
+
const inner = line.slice(i + delimiter.length, Math.min(end, line.length));
|
|
185
|
+
strings.push(delimiter.startsWith("'") ? inner : decodeBasicString(inner));
|
|
186
|
+
masked += `@${strings.length - 1}@`;
|
|
187
|
+
i = end + delimiter.length;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
masked += ch;
|
|
191
|
+
i += 1;
|
|
192
|
+
}
|
|
193
|
+
return { masked, strings };
|
|
194
|
+
}
|
|
195
|
+
const TOML_KEY_PART = String.raw `(?:[A-Za-z0-9_-]+|@\d+@)`;
|
|
196
|
+
const TOML_ASSIGNMENT = new RegExp(String.raw `^\s*(${TOML_KEY_PART}(?:\s*\.\s*${TOML_KEY_PART})*)\s*=\s*(.*?)\s*$`);
|
|
197
|
+
function unmaskKeyPart(part, strings) {
|
|
198
|
+
const token = part.match(/^@(\d+)@$/);
|
|
199
|
+
return token ? strings[Number(token[1])] : part;
|
|
200
|
+
}
|
|
201
|
+
export function parseMaskedAssignment(masked, strings) {
|
|
202
|
+
const match = masked.match(TOML_ASSIGNMENT);
|
|
203
|
+
return match
|
|
204
|
+
? {
|
|
205
|
+
path: match[1]
|
|
206
|
+
.split(".")
|
|
207
|
+
.map((part) => unmaskKeyPart(part.trim(), strings)),
|
|
208
|
+
value: match[2],
|
|
209
|
+
}
|
|
210
|
+
: null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The string a masked value starts with. Only the start counts, so a value
|
|
214
|
+
* with damage after it (a literal `\n` left by a bad paste) is still read.
|
|
215
|
+
*/
|
|
216
|
+
export function maskedStringValue(value, strings) {
|
|
217
|
+
const token = value.match(/^@(\d+)@/);
|
|
218
|
+
return token ? strings[Number(token[1])] : undefined;
|
|
219
|
+
}
|
|
220
|
+
/** The pairs directly inside a one-line inline table. */
|
|
221
|
+
export function inlineTablePairs(value, strings) {
|
|
222
|
+
if (!value.startsWith("{") || !value.endsWith("}")) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
const parts = [];
|
|
226
|
+
let depth = 0;
|
|
227
|
+
let start = 1;
|
|
228
|
+
for (let i = 1; i < value.length - 1; i += 1) {
|
|
229
|
+
if (value[i] === "{" || value[i] === "[") {
|
|
230
|
+
depth += 1;
|
|
231
|
+
}
|
|
232
|
+
else if (value[i] === "}" || value[i] === "]") {
|
|
233
|
+
depth -= 1;
|
|
234
|
+
}
|
|
235
|
+
else if (value[i] === "," && depth === 0) {
|
|
236
|
+
parts.push(value.slice(start, i));
|
|
237
|
+
start = i + 1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
parts.push(value.slice(start, value.length - 1));
|
|
241
|
+
return parts.flatMap((part) => {
|
|
242
|
+
const pair = parseMaskedAssignment(part, strings);
|
|
243
|
+
return pair ? [pair] : [];
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** The dotted key a statement assigns, and its value when that is a string. */
|
|
247
|
+
export function readTomlAssignment(line) {
|
|
248
|
+
const { masked, strings } = maskTomlStrings(line);
|
|
249
|
+
const assignment = parseMaskedAssignment(masked, strings);
|
|
250
|
+
return assignment
|
|
251
|
+
? {
|
|
252
|
+
path: assignment.path,
|
|
253
|
+
value: maskedStringValue(assignment.value, strings),
|
|
254
|
+
}
|
|
255
|
+
: null;
|
|
256
|
+
}
|
|
257
|
+
function isTomlContentLine(line) {
|
|
258
|
+
const text = line.trim();
|
|
259
|
+
return text.length > 0 && !text.startsWith("#");
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The lines of a section that are TOML rather than the inside of a
|
|
263
|
+
* multi-line string; `statement` marks those that start a key/value pair
|
|
264
|
+
* rather than continue an array or inline table.
|
|
265
|
+
*/
|
|
266
|
+
export function tomlCodeLines(section) {
|
|
267
|
+
const code = [];
|
|
268
|
+
let state = { multiline: null, depth: 0 };
|
|
269
|
+
section.lines.forEach((line, index) => {
|
|
270
|
+
const isHeader = index === 0 && section.path !== null;
|
|
271
|
+
if (!isHeader && state.multiline === null) {
|
|
272
|
+
code.push({ line, statement: state.depth === 0 });
|
|
273
|
+
}
|
|
274
|
+
if (!isHeader) {
|
|
275
|
+
state = advanceTomlScan(line, state);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
return code;
|
|
279
|
+
}
|
|
280
|
+
export function readStringValue(section, key) {
|
|
281
|
+
for (const { line, statement } of tomlCodeLines(section)) {
|
|
282
|
+
const assignment = statement ? readTomlAssignment(line) : null;
|
|
283
|
+
if (assignment?.path.length === 1 && assignment.path[0] === key) {
|
|
284
|
+
return assignment.value;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Names of the `<parent>.<name>` tables defined by dotted keys instead of a
|
|
291
|
+
* `[<parent>.<name>]` header: `claude-opus-4-6.base_url = ...` under
|
|
292
|
+
* `[model]`, or `model.claude-opus-4-6.base_url = ...` before the first
|
|
293
|
+
* header.
|
|
294
|
+
*/
|
|
295
|
+
export function dottedTableIds(sections, parent) {
|
|
296
|
+
return sections.flatMap((section) => {
|
|
297
|
+
const underParent = !section.arrayTable &&
|
|
298
|
+
section.path?.length === 1 &&
|
|
299
|
+
section.path[0] === parent;
|
|
300
|
+
if (!underParent && section.path !== null) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
return tomlCodeLines(section).flatMap(({ line, statement }) => {
|
|
304
|
+
const path = statement ? readTomlAssignment(line)?.path : undefined;
|
|
305
|
+
if (!path) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
if (underParent) {
|
|
309
|
+
return [path[0]];
|
|
310
|
+
}
|
|
311
|
+
return path.length >= 2 && path[0] === parent ? [path[1]] : [];
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* How many of an owned section's lines are the writer's. Comments after its
|
|
317
|
+
* last key belong to what follows, so a note the user wrote above their own
|
|
318
|
+
* next table survives. `managedLines` are the writer's own comment lines,
|
|
319
|
+
* which are dropped wherever they sit.
|
|
320
|
+
*/
|
|
321
|
+
export function ownedLineCount(section, managedLines) {
|
|
322
|
+
const lastKey = section.lines.findLastIndex(isTomlContentLine);
|
|
323
|
+
const firstNote = section.lines.findIndex((line, index) => index > lastKey &&
|
|
324
|
+
line.trim().length > 0 &&
|
|
325
|
+
!managedLines.has(line.trim()));
|
|
326
|
+
return firstNote === -1 ? section.lines.length : firstNote;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* A table declared twice is a TOML error, and neither Grok nor Codex will
|
|
330
|
+
* start on one.
|
|
331
|
+
* Each `[[array]]` element opens a fresh scope for everything under it, so
|
|
332
|
+
* `[mcp_servers.env]` under two elements is not a repeat.
|
|
333
|
+
*/
|
|
334
|
+
export function findDuplicateTable(text) {
|
|
335
|
+
const tables = new Map();
|
|
336
|
+
const arrays = new Map();
|
|
337
|
+
const isUnder = (candidate, prefix) => candidate.length > prefix.length &&
|
|
338
|
+
prefix.every((part, index) => candidate[index] === part);
|
|
339
|
+
for (const { path, arrayTable } of splitTomlSections(text)) {
|
|
340
|
+
if (path === null) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const key = JSON.stringify(path);
|
|
344
|
+
if (arrayTable) {
|
|
345
|
+
if (tables.has(key)) {
|
|
346
|
+
return path.join(".");
|
|
347
|
+
}
|
|
348
|
+
for (const scope of [tables, arrays]) {
|
|
349
|
+
for (const [seenKey, seenPath] of scope) {
|
|
350
|
+
if (isUnder(seenPath, path)) {
|
|
351
|
+
scope.delete(seenKey);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
arrays.set(key, path);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (tables.has(key) || arrays.has(key)) {
|
|
359
|
+
return path.join(".");
|
|
360
|
+
}
|
|
361
|
+
tables.set(key, path);
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
//# sourceMappingURL=tomlSections.js.map
|
|
@@ -29,8 +29,10 @@ export type CliProxyClientConfigurator = {
|
|
|
29
29
|
/**
|
|
30
30
|
* Point the CLI at the proxy. `proxyBaseUrl` is the bare proxy origin
|
|
31
31
|
* (e.g. "http://127.0.0.1:55669"); the configurator appends whatever path
|
|
32
|
-
* suffix its CLI needs. Returns false when
|
|
33
|
-
* never print a success message for work that did
|
|
32
|
+
* suffix its CLI needs. Returns false when the CLI is not left pointing at
|
|
33
|
+
* the proxy, so callers never print a success message for work that did
|
|
34
|
+
* not happen. A configurator whose file already matches may return true
|
|
35
|
+
* without rewriting it.
|
|
34
36
|
*/
|
|
35
37
|
apply: (proxyBaseUrl: string, options?: CliProxyClientApplyOptions) => Promise<boolean>;
|
|
36
38
|
/**
|
|
@@ -55,7 +57,10 @@ export type CliProxyClientConfigurator = {
|
|
|
55
57
|
export type CliProxyClientApplyResult = {
|
|
56
58
|
id: string;
|
|
57
59
|
displayName: string;
|
|
58
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* True only when the CLI's configuration points at the proxy: written now,
|
|
62
|
+
* or already written and left as it was.
|
|
63
|
+
*/
|
|
59
64
|
applied: boolean;
|
|
60
65
|
/**
|
|
61
66
|
* Set when the write landed but is not yet in effect โ see
|
|
@@ -110,6 +115,26 @@ export type CliGrokSnapshot = {
|
|
|
110
115
|
originalExisted: boolean;
|
|
111
116
|
writtenBaseUrl: string;
|
|
112
117
|
};
|
|
118
|
+
/**
|
|
119
|
+
* One header-delimited run of a client's TOML config. `path` is the parsed
|
|
120
|
+
* header key (`["model", "gemini-2.5-pro"]` for `[model."gemini-2.5-pro"]`),
|
|
121
|
+
* or null for the keys before the first header. `lines` keep their own
|
|
122
|
+
* terminators, so text the writer does not own round-trips byte for byte.
|
|
123
|
+
*/
|
|
124
|
+
export type CliTomlSection = {
|
|
125
|
+
path: readonly string[] | null;
|
|
126
|
+
arrayTable: boolean;
|
|
127
|
+
lines: readonly string[];
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* What a line of TOML leaves open for the next one: a multi-line string, or
|
|
131
|
+
* brackets of an array or inline table. A `[` that starts a line is a table
|
|
132
|
+
* header only when neither is open.
|
|
133
|
+
*/
|
|
134
|
+
export type CliTomlScanState = {
|
|
135
|
+
multiline: '"""' | "'''" | null;
|
|
136
|
+
depth: number;
|
|
137
|
+
};
|
|
113
138
|
/**
|
|
114
139
|
* Snapshot of the user's pre-existing Gemini CLI `~/.gemini/.env`.
|
|
115
140
|
*
|
|
@@ -2090,9 +2090,9 @@
|
|
|
2090
2090
|
{"objectID":"b67926d8a755ad92e57bdd16c8f17d1f7f0a0ce03c42865cb1f341959b146e2e","title":"Post-Migration","url":"/docs/development/factory-migration#post-migration","content":"[ ] Verify all provider functionality\n[ ] Confirm performance improvements\n[ ] Validate error handling behavior\n[ ] Test failover scenarios\n[ ] Monitor production metrics\n[ ] Document new patterns for team\n[ ] Clean up legacy code","hierarchy":{"lvl0":"Development","lvl1":"Factory Pattern Migration Guide","lvl2":"Post-Migration","lvl3":""}},
|
|
2091
2091
|
{"objectID":"70058ed2ee55a4afc9808da9da91ee30ac2e23f2ac1113720e9e2e60b1053f19","title":"Key Performance Indicators","url":"/docs/development/factory-migration#key-performance-indicators","content":"This comprehensive migration guide ensures a smooth transition to NeuroLink's factory pattern architecture, maximizing the benefits of standardized provider management while minimizing migration risks.","hierarchy":{"lvl0":"Development","lvl1":"Factory Pattern Migration Guide","lvl2":"Key Performance Indicators","lvl3":""}},
|
|
2092
2092
|
{"objectID":"01352a99c713cd63d938c086b918f9cfaebdf1246aec71e0bd851c194caf370a","title":"๐ Related Documentation","url":"/docs/development/factory-migration#-related-documentation","content":"System Architecture - Overall system design\nTesting Strategy - Quality assurance approaches\nContributing Guide - Development workflow\nAdvanced Patterns - Factory implementation details","hierarchy":{"lvl0":"Development","lvl1":"Factory Pattern Migration Guide","lvl2":"๐ Related Documentation","lvl3":""}},
|
|
2093
|
-
{"objectID":"b5593a1a523fdc5889c9f1f0eff5b9644c34bade7dd677fda08dbbc87f023986","title":"Development","url":"/docs/development","content":"Development\n\nContributing to NeuroLink and extending its capabilities for your specific needs.\n\n๐ฏ Development Hub\n\nThis section covers everything needed for contributing to NeuroLink, understanding its architecture, and extending its functionality.\nContributing โ How to contribute to NeuroLink, including setup, coding standards, and submission guidelines.\nTesting โ Comprehensive testing strategies, test suite organization, and validation procedures.\nArchitecture โ Deep dive into NeuroLink's architecture, design patterns, and system organization.\nFactory Pattern Migration โ Guide for upgrading from older architectures to the new unified factory pattern system.\nDocumentation Versioning โ Managing documentation versions across releases using mike for version control and deployment.\nAutomated Link Checking โ Automated validation of documentation links with CI/CD integration to prevent broken references.\n\n๐ Quick Development Setup\n\n๐๏ธ Architecture Overview\n\nNeuroLink uses a Factory Pattern architecture that provides:\n\nCore Components\n\nDesign Principles\nUnified Interface: All providers implement the same interface\nType Safety: Full TypeScript support with strict typing\nExtensibility: Easy to add new providers and tools\nPerformance: Optimized for production use\nReliability: Comprehensive error handling and fallbacks\n\n๐ง Development Features\n\nEnterprise Automation (72+ Commands)\n\nNeuroLink includes comprehensive automation for development:\n\nSmart Testing System\nAdaptive test selection based on code changes\nProvider validation across all AI services\nPerformance benchmarking and regression detection\nComprehensive coverage reporting\n\nAutomated Content Generation\nScreenshot automation for documentation\nVideo generation for demonstrations\nDocumentation synchronization across files\nAsset optimization and management\n\n๐งช Testing Philosophy\n\nNeuroLink uses a multi-layered testing approach:\n\nTest Categories\nUnit Tests - Individual component testing\nIntegration Tests - Provider and tool interaction\nEnd-to-End Tests - Complete workflow validation\nPerformance Tests - Speed and resource usage\nRegression Tests - Prevent breaking changes\n\nTest Organization\n\nRunning Tests\n\n๐จ Code Style & Standards\n\nTypeScript Configuration\nStrict mode enabled for maximum type safety\nPath mapping for clean imports\nESLint and Prettier for consistent formatting\nDocumentation comments for all public APIs\n\nNaming Conventions\nPascalCase for classes and interfaces\ncamelCase for functions and variables\nkebab-case for file names\nUPPER_CASE for constants\n\nFile Organization\n\n๐ Contribution Workflow\nSetup Development Environment\nCreate Feature Branch\nDevelopment Process\nCommit & Submit\n\n๐ Learning Resources\n\nArchitecture Deep Dive\nFactory Pattern Guide - Understanding the core architecture\nMCP Integration - Tool system implementation\nProvider Development - Adding new AI providers\n\nBest Practices\nError handling patterns and strategies\nPerformance optimization techniques\nTesting methodologies and coverage\nDocumentation standards and automation\n\nCommunity\nGitHub Discussions for questions and ideas\nIssue tracking for bugs and feature requests\nCode reviews for learning and improvement\nRelease notes for staying updated\n\n๐ Related Resources\nCLI Guide - Understanding the command-line interface\nSDK Reference - API implementation details\nAdvanced Features - Enterprise capabilities\nExamples - Practical implementations","hierarchy":{"lvl0":"Development","lvl1":"Development","lvl2":"","lvl3":""}},
|
|
2093
|
+
{"objectID":"b5593a1a523fdc5889c9f1f0eff5b9644c34bade7dd677fda08dbbc87f023986","title":"Development","url":"/docs/development","content":"Development\n\nContributing to NeuroLink and extending its capabilities for your specific needs.\n\n๐ฏ Development Hub\n\nThis section covers everything needed for contributing to NeuroLink, understanding its architecture, and extending its functionality.\nContributing โ How to contribute to NeuroLink, including setup, coding standards, and submission guidelines.\nTesting โ Comprehensive testing strategies, test suite organization, and validation procedures.\nLogging Guidelines โ Log levels, structured fields, redaction, and per-instance routing.\nArchitecture โ Deep dive into NeuroLink's architecture, design patterns, and system organization.\nFactory Pattern Migration โ Guide for upgrading from older architectures to the new unified factory pattern system.\nDocumentation Versioning โ Managing documentation versions across releases using mike for version control and deployment.\nAutomated Link Checking โ Automated validation of documentation links with CI/CD integration to prevent broken references.\n\n๐ Quick Development Setup\n\n๐๏ธ Architecture Overview\n\nNeuroLink uses a Factory Pattern architecture that provides:\n\nCore Components\n\nDesign Principles\nUnified Interface: All providers implement the same interface\nType Safety: Full TypeScript support with strict typing\nExtensibility: Easy to add new providers and tools\nPerformance: Optimized for production use\nReliability: Comprehensive error handling and fallbacks\n\n๐ง Development Features\n\nEnterprise Automation (72+ Commands)\n\nNeuroLink includes comprehensive automation for development:\n\nSmart Testing System\nAdaptive test selection based on code changes\nProvider validation across all AI services\nPerformance benchmarking and regression detection\nComprehensive coverage reporting\n\nAutomated Content Generation\nScreenshot automation for documentation\nVideo generation for demonstrations\nDocumentation synchronization across files\nAsset optimization and management\n\n๐งช Testing Philosophy\n\nNeuroLink uses a multi-layered testing approach:\n\nTest Categories\nUnit Tests - Individual component testing\nIntegration Tests - Provider and tool interaction\nEnd-to-End Tests - Complete workflow validation\nPerformance Tests - Speed and resource usage\nRegression Tests - Prevent breaking changes\n\nTest Organization\n\nRunning Tests\n\n๐จ Code Style & Standards\n\nTypeScript Configuration\nStrict mode enabled for maximum type safety\nPath mapping for clean imports\nESLint and Prettier for consistent formatting\nDocumentation comments for all public APIs\n\nNaming Conventions\nPascalCase for classes and interfaces\ncamelCase for functions and variables\nkebab-case for file names\nUPPER_CASE for constants\n\nFile Organization\n\n๐ Contribution Workflow\nSetup Development Environment\nCreate Feature Branch\nDevelopment Process\nCommit & Submit\n\n๐ Learning Resources\n\nArchitecture Deep Dive\nFactory Pattern Guide - Understanding the core architecture\nMCP Integration - Tool system implementation\nProvider Development - Adding new AI providers\n\nBest Practices\nError handling patterns and strategies\nPerformance optimization techniques\nTesting methodologies and coverage\nDocumentation standards and automation\n\nCommunity\nGitHub Discussions for questions and ideas\nIssue tracking for bugs and feature requests\nCode reviews for learning and improvement\nRelease notes for staying updated\n\n๐ Related Resources\nCLI Guide - Understanding the command-line interface\nSDK Reference - API implementation details\nAdvanced Features - Enterprise capabilities\nExamples - Practical implementations","hierarchy":{"lvl0":"Development","lvl1":"Development","lvl2":"","lvl3":""}},
|
|
2094
2094
|
{"objectID":"7e615005bdec5429e8e613c0d3c0defeb45247422fadd40a4fac3649c5a3933b","title":"Development","url":"/docs/development#development","content":"Contributing to NeuroLink and extending its capabilities for your specific needs.","hierarchy":{"lvl0":"Development","lvl1":"Development","lvl2":"Development","lvl3":""}},
|
|
2095
|
-
{"objectID":"90a5298bb0c032d275a7a07b66e9795f8de3267dc17a2cb2981093354ad6ac16","title":"๐ฏ Development Hub","url":"/docs/development#-development-hub","content":"This section covers everything needed for contributing to NeuroLink, understanding its architecture, and extending its functionality.\nContributing โ How to contribute to NeuroLink, including setup, coding standards, and submission guidelines.\nTesting โ Comprehensive testing strategies, test suite organization, and validation procedures.\nArchitecture โ Deep dive into NeuroLink's architecture, design patterns, and system organization.\nFactory Pattern Migration โ Guide for upgrading from older architectures to the new unified factory pattern system.\nDocumentation Versioning โ Managing documentation versions across releases using mike for version control and deployment.\nAutomated Link Checking โ Automated validation of documentation links with CI/CD integration to prevent broken references.","hierarchy":{"lvl0":"Development","lvl1":"Development","lvl2":"๐ฏ Development Hub","lvl3":""}},
|
|
2095
|
+
{"objectID":"90a5298bb0c032d275a7a07b66e9795f8de3267dc17a2cb2981093354ad6ac16","title":"๐ฏ Development Hub","url":"/docs/development#-development-hub","content":"This section covers everything needed for contributing to NeuroLink, understanding its architecture, and extending its functionality.\nContributing โ How to contribute to NeuroLink, including setup, coding standards, and submission guidelines.\nTesting โ Comprehensive testing strategies, test suite organization, and validation procedures.\nLogging Guidelines โ Log levels, structured fields, redaction, and per-instance routing.\nArchitecture โ Deep dive into NeuroLink's architecture, design patterns, and system organization.\nFactory Pattern Migration โ Guide for upgrading from older architectures to the new unified factory pattern system.\nDocumentation Versioning โ Managing documentation versions across releases using mike for version control and deployment.\nAutomated Link Checking โ Automated validation of documentation links with CI/CD integration to prevent broken references.","hierarchy":{"lvl0":"Development","lvl1":"Development","lvl2":"๐ฏ Development Hub","lvl3":""}},
|
|
2096
2096
|
{"objectID":"5946aa015605c5d07b2c06eb361f2a408413b013014f8ea53c1121e099767678","title":"๐ Quick Development Setup","url":"/docs/development#-quick-development-setup","content":"`bash","hierarchy":{"lvl0":"Development","lvl1":"Development","lvl2":"๐ Quick Development Setup","lvl3":""}},
|
|
2097
2097
|
{"objectID":"15a2f66b412fdff80552d6b65a8f2d4beea2bcb7be6be8118f6fcbf6e1cbc02c","title":"Clone the repository","url":"/docs/development#clone-the-repository","content":"git clone https://github.com/juspay/neurolink\ncd neurolink","hierarchy":{"lvl0":"Development","lvl1":"Development","lvl2":"Clone the repository","lvl3":""}},
|
|
2098
2098
|
{"objectID":"8a73475040b7c11aeadadf82943d4c99d644301c0c5b978a029392fc2f7b7d6f","title":"Install dependencies","url":"/docs/development#install-dependencies","content":"pnpm install","hierarchy":{"lvl0":"Development","lvl1":"Development","lvl2":"Install dependencies","lvl3":""}},
|
|
@@ -2240,6 +2240,17 @@
|
|
|
2240
2240
|
{"objectID":"8655bbc0db2d8853389936e04247ce18bf5f825c17810ab505c2212ae7ef88d1","title":"Build-time Link Checking","url":"/docs/development/link-checking#build-time-link-checking","content":"Add to :\n\nCreate :","hierarchy":{"lvl0":"Development","lvl1":"Automated Link Checking","lvl2":"Build-time Link Checking","lvl3":""}},
|
|
2241
2241
|
{"objectID":"4677ec51bc0b0f13c2e326aabf374218f42cf9fd3e2809acda6a932217461adb","title":"Related Documentation","url":"/docs/development/link-checking#related-documentation","content":"Versioning - Documentation version management\nContributing - Contribution guidelines\nTesting - Testing strategies","hierarchy":{"lvl0":"Development","lvl1":"Automated Link Checking","lvl2":"Related Documentation","lvl3":""}},
|
|
2242
2242
|
{"objectID":"44c7a422d2affcdb3f335b78d80b0958be5d760990185fb0d4505d45b8a9a325","title":"Additional Resources","url":"/docs/development/link-checking#additional-resources","content":"markdown-link-check - Link checker tool\nremark-validate-links - Alternative validator\nGitHub Actions - CI/CD automation","hierarchy":{"lvl0":"Development","lvl1":"Automated Link Checking","lvl2":"Additional Resources","lvl3":""}},
|
|
2243
|
+
{"objectID":"e111ff87d03c1f77a7d1932885150cd17c19ce903dce394a0514ed74e4bd0270","title":"Logging Guidelines","url":"/docs/development/logging-guidelines","content":"Logging Guidelines\n\nHow NeuroLink logs, and what a contributor has to get right. This documents the\nconvention the codebase already follows โ it is not a proposal to change it.\n\nThe logger\n\nImport the shared logger. Never call in : ESLint's\n rule fails the build for , and the exceptions for\n// exist for a handful of legacy call sites, not as an\ninvitation.\n\nThere is one logger for the whole process. attaches\nthe process-wide sink; per-instance events reach a worker only through its\n bridge (see per-instance routing below).\n\nLevels\n\n| Level | Use for |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |\n| | Routine operation: request construction, cache hits, resolution steps, internal state. The default for anything that fires per request. |\n| | Events an operator would want in a quiet log: a server binding, a provider registering, an MCP server connecting. Not per-request chatter. |\n| | Something recoverable happened and the code carried on: a retry, a fallback, a deprecated option, a degraded capability. |\n| | An operation failed, or a failure was swallowed and the caller will not see it. |\n\n bypasses level filtering and writes to the console\nunconditionally. It is CLI output, not logging โ user-facing text that must\nappear regardless of . Of ~2,260 call sites, ~2,150 are in .\nDo not reach for it in .\n\nNothing below is visible by default\n\n suppresses every level except unless debug mode is on\n(, or ). then sets the\nfloor within debug mode. Two consequences worth internalising:\nA log is not emitted in a normal run, but its arguments are still\n evaluated. Prefer over silence.\nA condition a user must act on cannot be reported at alone โ they will\n never see it. Either raise it to or surface it on the result object.\n\nChoosing between and \n\nThe common mistake is logging an ordinary outcome at . If a branch is\nreached on a healthy request, it is , however unwelcome it looks locally.\n is the worked example: a detection that lands below the\nconfidence threshold is the normal case for most files, so it logs at \nand says so in a comment. A detection where no strategy identified a type at\nall is a genuine failure, and logs at .\n\nFormat\n\nPass a message and a structured data object. The message is a constant; the\nvariables go in the object, where a log consumer can index them.\n\nPrefix the message with the emitting component in brackets โ ,\n, , . Grepping a debug run is how most of this\ngets read.\n\nTemplate literals are not banned, and a short one carrying a single value is\nfine. What matters is that a value a consumer would want to filter on ends up in\nthe data object rather than baked into the message string.\n\nGuard expensive serialization\n\n evaluates its arguments before ever runs, so a\n of a large payload costs full price on every request even when\nnothing is logged. Guard it:\n\nThe guard is only needed for work that is expensive to produce. Passing an\nobject you already hold is free โ the logger does not serialize it unless it\nemits.\n\nNever log secrets\n\nAPI keys, tokens, credentials, presigned URL query strings, user prompt content\nand absolute host paths must not reach a log or a thrown error message.\n has the helpers, and they are the reason several\npast leaks are closed:\n\n| Helper | Use for |\n| --------------------------- | ------------------------------------------------------------------------------------------ |\n| | A URL in a log or error โ strips query and fragment, so a presigned token cannot survive. |\n| | A URL that may carry . |\n| | A filesystem path in a message. |\n| | An error from Node/undici โ these embed the full request URL or path in their own message. |\n| | A header bag before logging it. |\n| | An arbitrary object, with string truncation. |\n| | A large value in a debug log, length-capped. |\n| | Tool-call parameters before logging them (this one lives in ). |\n\nThe rule for errors is the same as for logs: an error message is read by more\npeople than a log line, not fewer.\n\nPer-instance routing\n\nThe logger routes per instance, so a worker's bridge\n() receives only that worker's own\nevents, not everything in the process. The SDK entry points โ ,\n, โ run their bodies inside an \nscope carrying the instance's id, so a log call anywhere beneath them is\nattr","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"","lvl3":""}},
|
|
2244
|
+
{"objectID":"ec8f0114d488deec97f5a673a32bb2715f189d83292738c48c9fbc75b8344202","title":"Logging Guidelines","url":"/docs/development/logging-guidelines#logging-guidelines","content":"How NeuroLink logs, and what a contributor has to get right. This documents the\nconvention the codebase already follows โ it is not a proposal to change it.","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"Logging Guidelines","lvl3":""}},
|
|
2245
|
+
{"objectID":"5d174f91c3ccb88a59544c066c3cbf3a2116b97ffacd2414a51f4d64f46fda14","title":"The logger","url":"/docs/development/logging-guidelines#the-logger","content":"Import the shared logger. Never call in : ESLint's\n rule fails the build for , and the exceptions for\n// exist for a handful of legacy call sites, not as an\ninvitation.\n\nThere is one logger for the whole process. attaches\nthe process-wide sink; per-instance events reach a worker only through its\n bridge (see per-instance routing below).","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"The logger","lvl3":""}},
|
|
2246
|
+
{"objectID":"73c3d51ed27bc5ea62bb86ab9dde6cc2b17cecce7a0452a5aa7593da612cf798","title":"Levels","url":"/docs/development/logging-guidelines#levels","content":"| Level | Use for |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |\n| | Routine operation: request construction, cache hits, resolution steps, internal state. The default for anything that fires per request. |\n| | Events an operator would want in a quiet log: a server binding, a provider registering, an MCP server connecting. Not per-request chatter. |\n| | Something recoverable happened and the code carried on: a retry, a fallback, a deprecated option, a degraded capability. |\n| | An operation failed, or a failure was swallowed and the caller will not see it. |\n\n bypasses level filtering and writes to the console\nunconditionally. It is CLI output, not logging โ user-facing text that must\nappear regardless of . Of ~2,260 call sites, ~2,150 are in .\nDo not reach for it in .","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"Levels","lvl3":""}},
|
|
2247
|
+
{"objectID":"4dfd061a8a2bd9527c19e4f651358a6a1f9d1e6ecad7505f47dcc69e88ac5cd2","title":"Nothing below error is visible by default","url":"/docs/development/logging-guidelines#nothing-below-error-is-visible-by-default","content":"suppresses every level except unless debug mode is on\n(, or ). then sets the\nfloor within debug mode. Two consequences worth internalising:\nA log is not emitted in a normal run, but its arguments are still\n evaluated. Prefer over silence.\nA condition a user must act on cannot be reported at alone โ they will\n never see it. Either raise it to or surface it on the result object.","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"Nothing below error is visible by default","lvl3":""}},
|
|
2248
|
+
{"objectID":"19e83441e80eb578fc3e13da08db3aecd6953418f59ea037e40d543ff6f1e812","title":"Choosing between warn and debug","url":"/docs/development/logging-guidelines#choosing-between-warn-and-debug","content":"The common mistake is logging an ordinary outcome at . If a branch is\nreached on a healthy request, it is , however unwelcome it looks locally.\n is the worked example: a detection that lands below the\nconfidence threshold is the normal case for most files, so it logs at \nand says so in a comment. A detection where no strategy identified a type at\nall is a genuine failure, and logs at .","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"Choosing between warn and debug","lvl3":""}},
|
|
2249
|
+
{"objectID":"5e7a37fe452805fbf06c4864c7c4bedb538788640158dd499a2de613d4c12cae","title":"Format","url":"/docs/development/logging-guidelines#format","content":"Pass a message and a structured data object. The message is a constant; the\nvariables go in the object, where a log consumer can index them.\n\nPrefix the message with the emitting component in brackets โ ,\n, , . Grepping a debug run is how most of this\ngets read.\n\nTemplate literals are not banned, and a short one carrying a single value is\nfine. What matters is that a value a consumer would want to filter on ends up in\nthe data object rather than baked into the message string.","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"Format","lvl3":""}},
|
|
2250
|
+
{"objectID":"53c83ffa5114d21fc9998ca5d409a69d0c10feeedb061bc0f757ebd2d3a0d9eb","title":"Guard expensive serialization","url":"/docs/development/logging-guidelines#guard-expensive-serialization","content":"evaluates its arguments before ever runs, so a\n of a large payload costs full price on every request even when\nnothing is logged. Guard it:\n\nThe guard is only needed for work that is expensive to produce. Passing an\nobject you already hold is free โ the logger does not serialize it unless it\nemits.","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"Guard expensive serialization","lvl3":""}},
|
|
2251
|
+
{"objectID":"611da1cea62fd0b5bf313f3659615743913252c6ba487823808c87bee60efd00","title":"Never log secrets","url":"/docs/development/logging-guidelines#never-log-secrets","content":"API keys, tokens, credentials, presigned URL query strings, user prompt content\nand absolute host paths must not reach a log or a thrown error message.\n has the helpers, and they are the reason several\npast leaks are closed:\n\n| Helper | Use for |\n| --------------------------- | ------------------------------------------------------------------------------------------ |\n| | A URL in a log or error โ strips query and fragment, so a presigned token cannot survive. |\n| | A URL that may carry . |\n| | A filesystem path in a message. |\n| | An error from Node/undici โ these embed the full request URL or path in their own message. |\n| | A header bag before logging it. |\n| | An arbitrary object, with string truncation. |\n| | A large value in a debug log, length-capped. |\n| | Tool-call parameters before logging them (this one lives in ). |\n\nThe rule for errors is the same as for logs: an error message is read by more\npeople than a log line, not fewer.","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"Never log secrets","lvl3":""}},
|
|
2252
|
+
{"objectID":"c8bf38e894176c3436e15411303845f458ecf58f7bc7da0d105ecaf057adf7f4","title":"Per-instance routing","url":"/docs/development/logging-guidelines#per-instance-routing","content":"The logger routes per instance, so a worker's bridge\n() receives only that worker's own\nevents, not everything in the process. The SDK entry points โ ,\n, โ run their bodies inside an \nscope carrying the instance's id, so a log call anywhere beneath them is\nattributed without threading an instance through every call site. Two things\nsit outside the scope by construction: logs emitted while a consumer drains a\nreturned stream (iteration runs in the consumer's context), and logs emitted\noutside any call โ construction, background MCP reconnects, module init โ\nwhich stay unattributed rather than being charged to an arbitrary instance.\n\n's JSDoc () is\nthe authoritative description of this behaviour; check it before relying on\nthe routing in a new integration.\n\nA process-wide sink installed with still receives\neverything either way.","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"Per-instance routing","lvl3":""}},
|
|
2253
|
+
{"objectID":"fcd10d804aaa898a37f6d1303fbf6b07499f6bcd5b7e90e8f727aedec7b3f8e8","title":"Checklist for a new log call","url":"/docs/development/logging-guidelines#checklist-for-a-new-log-call","content":"Level matches the table above, and a routine branch is .\nMessage is a constant with a prefix; variables are in the data\n object.\nAnything expensive to build is behind .\nNo key, token, credential, prompt body, presigned URL or absolute host path\n in either the message or the data โ run it through if in\n doubt.","hierarchy":{"lvl0":"Development","lvl1":"Logging Guidelines","lvl2":"Checklist for a new log call","lvl3":""}},
|
|
2243
2254
|
{"objectID":"b7ea7ebd627e70d0c3debd74687c71a4ad7cb1a63a6b24e2a4818736e0129778","title":"Provider-Agnostic Testing Framework - January 2025 Snapshot","url":"/docs/development/provider-testing","content":"โ ๏ธ HISTORICAL DOCUMENT (January 2025) โ This is a snapshot of the provider-agnostic testing milestone when 9 providers were live. The current product ships 40 providers (incl. voice and the decision-only type). For the up-to-date provider list and capability matrix, see the README and Provider Capabilities Audit.\n\nProvider-Agnostic Testing Framework - January 2025 Snapshot\n\nUpdated: January 20, 2025 \nStatus: COMPLETE SUCCESS โ 9/9 providers verified working at the time of writing \nObjective: Complete provider testing after resolving critical configuration bug\n\n๐ฏ MISSION ACCOMPLISHED\n\nProblem Solved\n\nThe previous testing framework was hardcoded to Google AI, making it impossible to validate other providers during migration. This has been completely fixed.\n\nSolution Implemented\n\nโ
Provider-agnostic test runner \nโ
Configurable environment validation \nโ
Dynamic provider switching \nโ
Hugging Face implementation complete\nโ
Ready for comprehensive testing phase\n\n๐ง IMPLEMENTATION DETAILS\nEnhanced Test Runner ()\n\nProvider Configuration System\n\nUsage Examples\n\nEnvironment Validation\nโ
Automatic API key detection\nโ
Clear error messages for missing credentials\nโ
Provider-specific configuration validation\nโ
Dynamic environment variable setup\nProvider-Agnostic Test Files\n\nDynamic Provider Detection\n\nUpdated Test Files\nโ
- Provider-agnostic\nโ
- Provider-agnostic\n๐ Additional test files can be updated using same pattern\n\n๐งช VALIDATION RESULTS\n\nGoogle AI Provider Testing\n\nOpenAI Provider Testing\n\nKey Observations\nโ
Both providers pass all tests\nโ
OpenAI is slightly faster (6.15s vs 9.08s)\nโ
Same test suite validates both providers\nโ
No code changes needed between providers\n\n๐ STRATEGIC BENEFITS\nMigration Confidence\nBaseline Established: Google AI provider validated and working\nTarget Confirmed: OpenAI provider already operational\nTest Coverage: Universal test suite applies to all providers\nRegression Prevention: Any breaking changes immediately detected\nDevelopment Velocity\nParallel Testing: Can test multiple providers simultaneously\nQuick Validation: Individual provider testing in \\<10 seconds\nClear Feedback: Provider-specific error messages and success metrics\nAutomated Reports: JSON reports saved per provider\nQuality Assurance\nNo Manual Testing: Automated validation across all providers\nConsistent Coverage: Same test scenarios for all providers\nPerformance Monitoring: Response time tracking per provider\nEnvironment Validation: Automatic credential checking\n\n๐ NEXT STEPS FOR PHASE 3\n\nโ
Migration Complete - All Providers Operational\n\nWith the provider-agnostic testing framework and factory pattern complete:\n\nโ
Factory Pattern Implementation Complete\nโ
BaseProvider: All 9 providers (at the time of writing) extend BaseProvider (verified)\nโ
Custom Vercel AI SDK: Azure, HuggingFace, Ollama use custom implementations\nโ
Official Vercel AI SDK: OpenAI, Anthropic, Bedrock, Google AI, Mistral\nโ
100% Success Rate: All 9 providers in this snapshot tested and operational\n\nโ
Architecture Achievements\nโ
No External Package Issues: Custom implementations solve compatibility problems\nโ
Universal Analytics: Analytics helper integrated across all providers\nโ
Unified Interface: Single parameter handling system operational\nโ
Enterprise Ready: Complete factory-first MCP architecture\n\nTesting Strategy for Phase 3\n\n๐ฏ SUCCESS CRITERIA MET\n\nOriginal Requirements\nโ
Fix testing script to be provider agnostic\nโ
Test with OpenAI first (already implemented)\nโ
Validate provider-agnostic functionality working\n\nAdditional Achievements\nโ
Support for 4 providers (Google AI, OpenAI, Anthropic, Bedrock)\nโ
Automatic environment validation\nโ
Clear error messaging\nโ
Performance benchmarking\nโ
JSON report generation\n\n๐ CONCLUSION\n\nThe provider-agnostic testing framework is now complete and operational.\nProblem Solved: No longer bound to Google AI\nQuality Assured: Both existing providers validated\nFoundation Ready: Perfect infrastructure for Phase 3 migration\nDevelopment Ready: Can proceed with confidence\n\nWe can now begin Phase 3 migration knowing that every step can be validated immediately with comprehensive, provider-agnostic testing.","hierarchy":{"lvl0":"Development","lvl1":"Provider-Agnostic Testing Framework - January 2025 Snapshot","lvl2":"","lvl3":""}},
|
|
2244
2255
|
{"objectID":"40155d0164fbc188b20eb8158f339edd47393bae047ec9d23dc1a22745059fa8","title":"Provider-Agnostic Testing Framework - January 2025 Snapshot","url":"/docs/development/provider-testing#provider-agnostic-testing-framework---january-2025-snapshot","content":"Updated: January 20, 2025 \nStatus: COMPLETE SUCCESS โ 9/9 providers verified working at the time of writing \nObjective: Complete provider testing after resolving critical configuration bug","hierarchy":{"lvl0":"Development","lvl1":"Provider-Agnostic Testing Framework - January 2025 Snapshot","lvl2":"Provider-Agnostic Testing Framework - January 2025 Snapshot","lvl3":""}},
|
|
2245
2256
|
{"objectID":"3212634220f7620a7f5dd8d7ffa3a5fb8b359e4c6722504ae1d7881d0020a6da","title":"Problem Solved","url":"/docs/development/provider-testing#problem-solved","content":"The previous testing framework was hardcoded to Google AI, making it impossible to validate other providers during migration. This has been completely fixed.","hierarchy":{"lvl0":"Development","lvl1":"Provider-Agnostic Testing Framework - January 2025 Snapshot","lvl2":"Problem Solved","lvl3":""}},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.30.
|
|
3
|
+
"version": "12.30.2",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types โ generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI ยท Anthropic ยท Gemini ยท Bedrock ยท Azure ยท Ollama ยท TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|
|
@@ -275,7 +275,8 @@
|
|
|
275
275
|
"test:file-tool-roots": "pnpm exec tsx test/continuous-test-suite-file-tool-roots.ts",
|
|
276
276
|
"test:native-failure-events": "pnpm exec tsx test/continuous-test-suite-native-failure-events.ts",
|
|
277
277
|
"test:native-audio-model-aware": "pnpm exec tsx test/continuous-test-suite-native-audio-model-aware.ts",
|
|
278
|
-
"test:cli-json-output": "pnpm exec tsx test/continuous-test-suite-cli-json-output.ts"
|
|
278
|
+
"test:cli-json-output": "pnpm exec tsx test/continuous-test-suite-cli-json-output.ts",
|
|
279
|
+
"test:logging-guidelines": "pnpm exec tsx test/continuous-test-suite-logging-guidelines.ts"
|
|
279
280
|
},
|
|
280
281
|
"files": [
|
|
281
282
|
"dist",
|