@remnic/core 9.3.671 → 9.3.673
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/dist/access-audit.js +2 -2
- package/dist/access-cli.js +27 -25
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.js +12 -12
- package/dist/access-mcp.js +11 -11
- package/dist/access-schema.d.ts +36 -36
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +9 -9
- package/dist/active-recall.js +3 -1
- package/dist/active-recall.js.map +1 -1
- package/dist/chunk-3BQOQYRB.js +33 -0
- package/dist/chunk-3BQOQYRB.js.map +1 -0
- package/dist/{chunk-UOBLE67F.js → chunk-3IE22DJ2.js} +4 -4
- package/dist/chunk-52LZ42LI.js +25 -0
- package/dist/chunk-52LZ42LI.js.map +1 -0
- package/dist/{chunk-CRO4LCQ6.js → chunk-7OGJQP7T.js} +5 -5
- package/dist/{chunk-23EBQ27U.js → chunk-B55KFEGS.js} +2 -2
- package/dist/{chunk-BTLNC5YM.js → chunk-GNAMDNGT.js} +5 -13
- package/dist/chunk-GNAMDNGT.js.map +1 -0
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-MLVMBV2C.js → chunk-IUZWBCJX.js} +8 -40
- package/dist/chunk-IUZWBCJX.js.map +1 -0
- package/dist/{chunk-PYTATYUV.js → chunk-ODWI5XU2.js} +2 -2
- package/dist/{chunk-F7OWUP3G.js → chunk-OG7A6AZX.js} +4 -4
- package/dist/{chunk-XS2CWEHZ.js → chunk-Q6MIDQEL.js} +2 -2
- package/dist/{chunk-UDJLF3BO.js → chunk-QLRYXOAD.js} +2 -2
- package/dist/chunk-QO3AILZN.js +89 -0
- package/dist/chunk-QO3AILZN.js.map +1 -0
- package/dist/{chunk-ZI6A7X4V.js → chunk-R37A3BEW.js} +26 -26
- package/dist/{chunk-CPVV2UEL.js → chunk-SDLJ2W7S.js} +6 -6
- package/dist/{chunk-Z4GALEO3.js → chunk-SF45RQDX.js} +3 -3
- package/dist/{chunk-LXVOZ2O6.js → chunk-T2AOOHDA.js} +2 -2
- package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
- package/dist/{chunk-32RD3GIW.js → chunk-XVVEKF5I.js} +19 -19
- package/dist/{chunk-TGN4M5MB.js → chunk-ZLINDOBG.js} +4 -4
- package/dist/cli.js +24 -22
- package/dist/coding/optional-coding-graph.d.ts +63 -0
- package/dist/coding/optional-coding-graph.js +119 -0
- package/dist/coding/optional-coding-graph.js.map +1 -0
- package/dist/coding-graph-types-Dd2tGrnm.d.ts +106 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +3 -1
- package/dist/connectors/index.d.ts +6 -2
- package/dist/connectors/index.js +6 -2
- package/dist/conversation-index/backend.js +2 -2
- package/dist/emit-legacy-tools.d.ts +61 -0
- package/dist/emit-legacy-tools.js +12 -0
- package/dist/emit-legacy-tools.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +54 -46
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +2 -2
- package/dist/namespaces/migrate.js +5 -5
- package/dist/namespaces/search.js +4 -4
- package/dist/operator-toolkit.js +10 -8
- package/dist/orchestrator.js +16 -16
- package/dist/resume-bundles.js +4 -2
- package/dist/schemas.d.ts +42 -42
- package/dist/search/factory.js +3 -3
- package/dist/search/index.js +3 -3
- package/dist/shared-context/manager.d.ts +2 -2
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/package.json +19 -1
- package/src/coding/coding-graph-types.ts +180 -0
- package/src/coding/optional-coding-graph-cache.test.ts +86 -0
- package/src/coding/optional-coding-graph-cacheread.test.ts +78 -0
- package/src/coding/optional-coding-graph-concurrent.test.ts +48 -0
- package/src/coding/optional-coding-graph-incompatible.test.ts +98 -0
- package/src/coding/optional-coding-graph-probe.test.ts +34 -0
- package/src/coding/optional-coding-graph.test.ts +117 -0
- package/src/coding/optional-coding-graph.ts +370 -0
- package/src/config.test.ts +408 -6
- package/src/config.ts +12 -56
- package/src/connectors/index.ts +2 -15
- package/src/connectors/paths.ts +50 -0
- package/src/emit-legacy-tools.test.ts +297 -0
- package/src/emit-legacy-tools.ts +204 -0
- package/src/index.ts +22 -0
- package/dist/chunk-BTLNC5YM.js.map +0 -1
- package/dist/chunk-MLVMBV2C.js.map +0 -1
- /package/dist/{chunk-UOBLE67F.js.map → chunk-3IE22DJ2.js.map} +0 -0
- /package/dist/{chunk-CRO4LCQ6.js.map → chunk-7OGJQP7T.js.map} +0 -0
- /package/dist/{chunk-23EBQ27U.js.map → chunk-B55KFEGS.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-PYTATYUV.js.map → chunk-ODWI5XU2.js.map} +0 -0
- /package/dist/{chunk-F7OWUP3G.js.map → chunk-OG7A6AZX.js.map} +0 -0
- /package/dist/{chunk-XS2CWEHZ.js.map → chunk-Q6MIDQEL.js.map} +0 -0
- /package/dist/{chunk-UDJLF3BO.js.map → chunk-QLRYXOAD.js.map} +0 -0
- /package/dist/{chunk-ZI6A7X4V.js.map → chunk-R37A3BEW.js.map} +0 -0
- /package/dist/{chunk-CPVV2UEL.js.map → chunk-SDLJ2W7S.js.map} +0 -0
- /package/dist/{chunk-Z4GALEO3.js.map → chunk-SF45RQDX.js.map} +0 -0
- /package/dist/{chunk-LXVOZ2O6.js.map → chunk-T2AOOHDA.js.map} +0 -0
- /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
- /package/dist/{chunk-32RD3GIW.js.map → chunk-XVVEKF5I.js.map} +0 -0
- /package/dist/{chunk-TGN4M5MB.js.map → chunk-ZLINDOBG.js.map} +0 -0
package/src/config.test.ts
CHANGED
|
@@ -1,14 +1,257 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import test from "node:test";
|
|
3
6
|
|
|
4
7
|
import { parseConfig } from "./config.js";
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Run `body` with XDG_CONFIG_HOME pointing at a throwaway dir so the
|
|
11
|
+
* sticky-legacy `emitLegacyTools` default (#1550) never reads the real
|
|
12
|
+
* machine state. `withLegacyEntry` seeds one persisted connector file.
|
|
13
|
+
*/
|
|
14
|
+
function withIsolatedConnectorsDir<T>(
|
|
15
|
+
withLegacyEntry: boolean,
|
|
16
|
+
body: () => T,
|
|
17
|
+
): T {
|
|
18
|
+
const prev = process.env.XDG_CONFIG_HOME;
|
|
19
|
+
const root = mkdtempSync(path.join(tmpdir(), "remnic-config-test-"));
|
|
20
|
+
process.env.XDG_CONFIG_HOME = root;
|
|
21
|
+
try {
|
|
22
|
+
if (withLegacyEntry) {
|
|
23
|
+
const dir = path.join(root, "engram", ".engram-connectors", "connectors");
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
writeFileSync(path.join(dir, "codex-cli.json"), "{}\n");
|
|
26
|
+
}
|
|
27
|
+
return body();
|
|
28
|
+
} finally {
|
|
29
|
+
if (prev === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
30
|
+
else process.env.XDG_CONFIG_HOME = prev;
|
|
31
|
+
rmSync(root, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("parseConfig emitLegacyTools sticky-legacy default (issue #1550)", () => {
|
|
36
|
+
// Fresh install (no legacy connector entries): canonical-only surface.
|
|
37
|
+
withIsolatedConnectorsDir(false, () => {
|
|
38
|
+
assert.equal(parseConfig({}).emitLegacyTools, false, "fresh install defaults false");
|
|
39
|
+
assert.equal(parseConfig({ emitLegacyTools: null }).emitLegacyTools, false);
|
|
40
|
+
});
|
|
41
|
+
// Existing install with a persisted legacy connector entry: aliases stay on.
|
|
42
|
+
withIsolatedConnectorsDir(true, () => {
|
|
43
|
+
assert.equal(parseConfig({}).emitLegacyTools, true, "legacy connector entry keeps aliases");
|
|
44
|
+
// Explicit opt-out still wins over the sticky evidence.
|
|
45
|
+
assert.equal(parseConfig({ emitLegacyTools: false }).emitLegacyTools, false);
|
|
46
|
+
assert.equal(parseConfig({ emitLegacyTools: "false" }).emitLegacyTools, false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("parseConfig emitLegacyTools raw-vs-effective: schema default does not block sticky legacy (#1550, PR #1593 review)", () => {
|
|
51
|
+
// Cursor Bugbot + chatgpt-codex-connector both flagged the same class: the
|
|
52
|
+
// OpenClaw SDK materializes JSON-schema defaults into `api.pluginConfig`
|
|
53
|
+
// BEFORE `parseConfig` runs, so a fresh install would arrive with
|
|
54
|
+
// `emitLegacyTools: false` already populated even though the operator never
|
|
55
|
+
// wrote the key. The original resolver treated any present value as
|
|
56
|
+
// operator-set, which short-circuited the sticky-legacy fallback to
|
|
57
|
+
// `hasLegacyConnectorEntries()` and broke upgrades with legacy connector
|
|
58
|
+
// entries on disk. Fix: parseConfig now takes an optional `rawOperatorConfig`
|
|
59
|
+
// argument (sourced from `loadPluginConfigFromFile`, the pre-defaults
|
|
60
|
+
// operator file). Resolvers check `rawOperatorConfig` first — if the key is
|
|
61
|
+
// absent there, the present value is treated as a schema default and the
|
|
62
|
+
// env / sticky-legacy fallback chain runs.
|
|
63
|
+
withIsolatedConnectorsDir(false, () => {
|
|
64
|
+
// Fresh install + schema-default false in the merged config: sticky
|
|
65
|
+
// legacy path should still run and resolve to false.
|
|
66
|
+
assert.equal(
|
|
67
|
+
parseConfig({ emitLegacyTools: false }, {}).emitLegacyTools,
|
|
68
|
+
false,
|
|
69
|
+
"fresh install with schema-default false still resolves false",
|
|
70
|
+
);
|
|
71
|
+
// Explicit operator opt-out (rawOperatorConfig has the key): false wins.
|
|
72
|
+
assert.equal(
|
|
73
|
+
parseConfig({ emitLegacyTools: false }, { emitLegacyTools: false })
|
|
74
|
+
.emitLegacyTools,
|
|
75
|
+
false,
|
|
76
|
+
"explicit operator opt-out via file is honored",
|
|
77
|
+
);
|
|
78
|
+
// When raw author wrote a real value but the merged configValue
|
|
79
|
+
// disagrees, configValue wins (matches the runtime-over-file spread in
|
|
80
|
+
// src/index.ts — chatgpt-codex-connector P2, PR #1593 round 4).
|
|
81
|
+
// coerceBooleanLikeOrThrow normalizes string "false" to boolean false.
|
|
82
|
+
assert.equal(
|
|
83
|
+
parseConfig({ emitLegacyTools: "false" }, { emitLegacyTools: "true" })
|
|
84
|
+
.emitLegacyTools,
|
|
85
|
+
false,
|
|
86
|
+
"merged value wins over raw value (runtime-over-file precedence)",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
// Upgraded install with legacy connector JSON on disk: schema-default
|
|
90
|
+
// `false` in merged config MUST NOT mask the sticky-legacy fallback. This
|
|
91
|
+
// is the exact Cursor Bugbot scenario.
|
|
92
|
+
withIsolatedConnectorsDir(true, () => {
|
|
93
|
+
assert.equal(
|
|
94
|
+
parseConfig({ emitLegacyTools: false }, {}).emitLegacyTools,
|
|
95
|
+
true,
|
|
96
|
+
"upgraded install with legacy connector JSON keeps aliases on",
|
|
97
|
+
);
|
|
98
|
+
// Raw operator opt-out still wins over the sticky evidence.
|
|
99
|
+
assert.equal(
|
|
100
|
+
parseConfig({ emitLegacyTools: false }, { emitLegacyTools: false })
|
|
101
|
+
.emitLegacyTools,
|
|
102
|
+
false,
|
|
103
|
+
"raw operator opt-out overrides sticky-legacy",
|
|
104
|
+
);
|
|
105
|
+
// Raw operator opt-IN also wins.
|
|
106
|
+
assert.equal(
|
|
107
|
+
parseConfig({ emitLegacyTools: true }, {}).emitLegacyTools,
|
|
108
|
+
true,
|
|
109
|
+
"raw merged true is operator-set even with empty raw",
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("parseConfig namespaceCatalogEnabled raw-vs-effective: schema-default hardening (#1550 class hardening)", () => {
|
|
115
|
+
// Same-class hardening as emitLegacyTools: if a future schema revision flips
|
|
116
|
+
// namespaceCatalogEnabled's default to `false`, the resolver must still let
|
|
117
|
+
// the sticky / absent chain run unless the operator wrote the key. Today
|
|
118
|
+
// the schema default is `true`, so the bug doesn't manifest — but the helper
|
|
119
|
+
// now has the raw-vs-effective split, and this test pins the contract so a
|
|
120
|
+
// future schema flip can't reintroduce the bug class silently.
|
|
121
|
+
//
|
|
122
|
+
// Logic: when raw is missing the key, compare merged to the schema default
|
|
123
|
+
// (true). If merged equals the schema default, it's the materialized
|
|
124
|
+
// schema value — fall through to default. If merged differs (i.e. merged
|
|
125
|
+
// is `false`), it's runtime operator intent — honor it. Symmetric to
|
|
126
|
+
// resolveEmitLegacyTools, with the schema default inverted.
|
|
127
|
+
// Absent from both → schema default true (sticky chain returns true).
|
|
128
|
+
assert.equal(
|
|
129
|
+
parseConfig({}, {}).namespaceCatalogEnabled,
|
|
130
|
+
true,
|
|
131
|
+
"absent from both raw and merged -> true (schema default)",
|
|
132
|
+
);
|
|
133
|
+
// Merged `true` (schema default) with empty raw → fall through, return true.
|
|
134
|
+
assert.equal(
|
|
135
|
+
parseConfig({ namespaceCatalogEnabled: true }, {}).namespaceCatalogEnabled,
|
|
136
|
+
true,
|
|
137
|
+
"merged true (schema default) with empty raw falls through to default",
|
|
138
|
+
);
|
|
139
|
+
// Merged `false` (DIFFERENT from schema default) with empty raw → runtime
|
|
140
|
+
// opt-out intent — honor it, return false.
|
|
141
|
+
assert.equal(
|
|
142
|
+
parseConfig({ namespaceCatalogEnabled: false }, {}).namespaceCatalogEnabled,
|
|
143
|
+
false,
|
|
144
|
+
"merged false differs from schema default — runtime operator intent honored",
|
|
145
|
+
);
|
|
146
|
+
// Merged value wins over raw value when both are present — matches
|
|
147
|
+
// the runtime-over-file spread in src/index.ts.
|
|
148
|
+
assert.equal(
|
|
149
|
+
parseConfig({ namespaceCatalogEnabled: true }, { namespaceCatalogEnabled: false })
|
|
150
|
+
.namespaceCatalogEnabled,
|
|
151
|
+
true,
|
|
152
|
+
"merged true wins over raw false (runtime-over-file)",
|
|
153
|
+
);
|
|
154
|
+
assert.equal(
|
|
155
|
+
parseConfig({ namespaceCatalogEnabled: false }, { namespaceCatalogEnabled: true })
|
|
156
|
+
.namespaceCatalogEnabled,
|
|
157
|
+
false,
|
|
158
|
+
"merged false wins over raw true (runtime-over-file)",
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("parseConfig emitLegacyTools raw null is treated as absent (PR #1593 review round 2)", () => {
|
|
163
|
+
// Cursor Bugbot round 2: when raw has the key but its value is `null`
|
|
164
|
+
// (operator explicitly cleared it in openclaw.json), the old resolver
|
|
165
|
+
// threw via coerceBooleanLikeOrThrow. New behavior: treat null/undefined
|
|
166
|
+
// in raw as "absent" and fall through to merged / env / sticky-legacy.
|
|
167
|
+
withIsolatedConnectorsDir(false, () => {
|
|
168
|
+
// Fresh install: raw null + merged null + env absent + sticky false → false.
|
|
169
|
+
assert.equal(
|
|
170
|
+
parseConfig({ emitLegacyTools: null }, { emitLegacyTools: null }).emitLegacyTools,
|
|
171
|
+
false,
|
|
172
|
+
"fresh install with raw null resolves to false via sticky-legacy",
|
|
173
|
+
);
|
|
174
|
+
// Legacy install: raw null + sticky evidence → true.
|
|
175
|
+
});
|
|
176
|
+
withIsolatedConnectorsDir(true, () => {
|
|
177
|
+
assert.equal(
|
|
178
|
+
parseConfig({ emitLegacyTools: null }, { emitLegacyTools: null }).emitLegacyTools,
|
|
179
|
+
true,
|
|
180
|
+
"upgraded install with raw null resolves to true via sticky-legacy",
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("parseConfig emitLegacyTools runtime true overrides schema default (PR #1593 review round 2)", () => {
|
|
186
|
+
// Cursor Bugbot round 2: schema default is `false`. When raw is missing
|
|
187
|
+
// the key but merged carries `true` (runtime gateway set it), the old
|
|
188
|
+
// resolver dropped the runtime override as schema-default materialization.
|
|
189
|
+
// New behavior: if merged value differs from the schema default, treat
|
|
190
|
+
// it as runtime operator intent and honor it.
|
|
191
|
+
withIsolatedConnectorsDir(false, () => {
|
|
192
|
+
// Merged `true` with empty raw → runtime opt-in honored, even on a
|
|
193
|
+
// fresh install (no legacy connector entries).
|
|
194
|
+
assert.equal(
|
|
195
|
+
parseConfig({ emitLegacyTools: true }, {}).emitLegacyTools,
|
|
196
|
+
true,
|
|
197
|
+
"runtime true with empty raw treated as operator intent",
|
|
198
|
+
);
|
|
199
|
+
// Merged `false` (the schema default) with empty raw → sticky fallback.
|
|
200
|
+
assert.equal(
|
|
201
|
+
parseConfig({ emitLegacyTools: false }, {}).emitLegacyTools,
|
|
202
|
+
false,
|
|
203
|
+
"merged false (schema default) with empty raw falls through to sticky-legacy",
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("parseConfig defensive null rawOperatorConfig (PR #1593 review round 3)", () => {
|
|
209
|
+
// Cursor Bugbot + kilo-code-bot round 3: a JSON `null` on disk for the
|
|
210
|
+
// operator config block surfaces as `null` at the second argument (the
|
|
211
|
+
// loader's `as Record | undefined` cast previously hid this). Both
|
|
212
|
+
// resolvers now normalize `null` to `{}` so the `"key" in raw` check
|
|
213
|
+
// never throws and the schema-default-detection logic still runs.
|
|
214
|
+
withIsolatedConnectorsDir(false, () => {
|
|
215
|
+
// null raw + emitLegacyTools merged schema-default false → sticky-legacy
|
|
216
|
+
// fallback (no throw).
|
|
217
|
+
assert.equal(
|
|
218
|
+
parseConfig({ emitLegacyTools: false }, null as unknown as Record<string, unknown>).emitLegacyTools,
|
|
219
|
+
false,
|
|
220
|
+
"null raw with emitLegacyTools merged schema default falls through to sticky-legacy",
|
|
221
|
+
);
|
|
222
|
+
// null raw + emitLegacyTools merged true (runtime intent) → honored.
|
|
223
|
+
assert.equal(
|
|
224
|
+
parseConfig({ emitLegacyTools: true }, null as unknown as Record<string, unknown>).emitLegacyTools,
|
|
225
|
+
true,
|
|
226
|
+
"null raw with emitLegacyTools runtime true honored as operator intent",
|
|
227
|
+
);
|
|
228
|
+
// null raw + namespaceCatalogEnabled merged false → differs from schema
|
|
229
|
+
// default true → runtime intent → honored.
|
|
230
|
+
assert.equal(
|
|
231
|
+
parseConfig({ namespaceCatalogEnabled: false }, null as unknown as Record<string, unknown>).namespaceCatalogEnabled,
|
|
232
|
+
false,
|
|
233
|
+
"null raw with namespaceCatalogEnabled runtime false honored as operator intent",
|
|
234
|
+
);
|
|
235
|
+
// null raw + namespaceCatalogEnabled merged true (schema default) →
|
|
236
|
+
// equals schema default → fall through to return SCHEMA_DEFAULT.
|
|
237
|
+
assert.equal(
|
|
238
|
+
parseConfig({ namespaceCatalogEnabled: true }, null as unknown as Record<string, unknown>).namespaceCatalogEnabled,
|
|
239
|
+
true,
|
|
240
|
+
"null raw with namespaceCatalogEnabled schema default true preserved",
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
withIsolatedConnectorsDir(true, () => {
|
|
244
|
+
// null raw + emitLegacyTools merged false + legacy evidence →
|
|
245
|
+
// sticky-legacy returns true (upgraded install scenario).
|
|
246
|
+
assert.equal(
|
|
247
|
+
parseConfig({ emitLegacyTools: false }, null as unknown as Record<string, unknown>).emitLegacyTools,
|
|
248
|
+
true,
|
|
249
|
+
"null raw with legacy evidence keeps aliases on",
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("parseConfig emitLegacyTools coerces config/env (issue #1427)", () => {
|
|
12
255
|
// Boolean + boolean-like string config values.
|
|
13
256
|
assert.equal(parseConfig({ emitLegacyTools: false }).emitLegacyTools, false);
|
|
14
257
|
assert.equal(parseConfig({ emitLegacyTools: "false" }).emitLegacyTools, false);
|
|
@@ -26,6 +269,12 @@ test("parseConfig emitLegacyTools defaults to true and coerces config/env (issue
|
|
|
26
269
|
delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
27
270
|
process.env.ENGRAM_EMIT_LEGACY_TOOLS = "false";
|
|
28
271
|
assert.equal(parseConfig({}).emitLegacyTools, false, "ENGRAM_ env fallback disables");
|
|
272
|
+
// Env "true" also wins over the fresh-install default (#1550).
|
|
273
|
+
delete process.env.ENGRAM_EMIT_LEGACY_TOOLS;
|
|
274
|
+
process.env.REMNIC_EMIT_LEGACY_TOOLS = "true";
|
|
275
|
+
withIsolatedConnectorsDir(false, () => {
|
|
276
|
+
assert.equal(parseConfig({}).emitLegacyTools, true, "env true wins over fresh-install default");
|
|
277
|
+
});
|
|
29
278
|
} finally {
|
|
30
279
|
if (prevRemnic === undefined) delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
31
280
|
else process.env.REMNIC_EMIT_LEGACY_TOOLS = prevRemnic;
|
|
@@ -1518,3 +1767,156 @@ test("parseConfig rejects invalid maintenance namespace fanout values", () => {
|
|
|
1518
1767
|
/maintenance\.namespaceLockStaleMs must be a positive integer/,
|
|
1519
1768
|
);
|
|
1520
1769
|
});
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
test("parseConfig runtime-over-file precedence (PR #1593 review round 4)", () => {
|
|
1773
|
+
// chatgpt-codex-connector P2: src/index.ts calls parseConfig with
|
|
1774
|
+
// { ...fileConfig, ...api.pluginConfig } where the spread means runtime
|
|
1775
|
+
// overrides file. The resolver must honor configValue (the merged
|
|
1776
|
+
// object) as authoritative when both raw and configValue are present.
|
|
1777
|
+
// raw is consulted only to detect "operator authored" vs "schema-default
|
|
1778
|
+
// materialization" — NOT to override configValue.
|
|
1779
|
+
//
|
|
1780
|
+
// Scenario: file says emitLegacyTools: false, runtime says
|
|
1781
|
+
// api.pluginConfig.emitLegacyTools: true. The merged configValue is
|
|
1782
|
+
// true; the resolver must honor the runtime override, not the file.
|
|
1783
|
+
withIsolatedConnectorsDir(false, () => {
|
|
1784
|
+
assert.equal(
|
|
1785
|
+
parseConfig(
|
|
1786
|
+
{ emitLegacyTools: true }, // merged: runtime over file
|
|
1787
|
+
{ emitLegacyTools: false }, // file wrote false
|
|
1788
|
+
).emitLegacyTools,
|
|
1789
|
+
true,
|
|
1790
|
+
"runtime true overrides file false (rawOperatorConfig has the key)",
|
|
1791
|
+
);
|
|
1792
|
+
// Symmetric: file says true, runtime says false → merged false wins.
|
|
1793
|
+
assert.equal(
|
|
1794
|
+
parseConfig(
|
|
1795
|
+
{ emitLegacyTools: false },
|
|
1796
|
+
{ emitLegacyTools: true },
|
|
1797
|
+
).emitLegacyTools,
|
|
1798
|
+
false,
|
|
1799
|
+
"runtime false overrides file true",
|
|
1800
|
+
);
|
|
1801
|
+
// Same precedence for namespaceCatalogEnabled.
|
|
1802
|
+
assert.equal(
|
|
1803
|
+
parseConfig(
|
|
1804
|
+
{ namespaceCatalogEnabled: false }, // runtime override to false
|
|
1805
|
+
{ namespaceCatalogEnabled: true }, // file set true (schema default)
|
|
1806
|
+
).namespaceCatalogEnabled,
|
|
1807
|
+
false,
|
|
1808
|
+
"runtime false overrides file true for namespaceCatalogEnabled",
|
|
1809
|
+
);
|
|
1810
|
+
});
|
|
1811
|
+
// Sticky-legacy still works: merged false (schema default), raw empty
|
|
1812
|
+
// → fall through to env / sticky-legacy. With legacy connector JSON on
|
|
1813
|
+
// disk → returns true (the upgraded install scenario).
|
|
1814
|
+
withIsolatedConnectorsDir(true, () => {
|
|
1815
|
+
assert.equal(
|
|
1816
|
+
parseConfig(
|
|
1817
|
+
{ emitLegacyTools: false },
|
|
1818
|
+
{}, // no file
|
|
1819
|
+
).emitLegacyTools,
|
|
1820
|
+
true,
|
|
1821
|
+
"merged false with empty raw falls through to sticky-legacy (upgraded install)",
|
|
1822
|
+
);
|
|
1823
|
+
});
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
test("parseConfig null raw + runtime override honored (PR #1593 review round 5)", () => {
|
|
1828
|
+
// Cursor Bugbot + chatgpt-codex-connector P2 (round 5, flagged against
|
|
1829
|
+
// round-3 commit ee58f92f/34316d5e): when raw has the key with a
|
|
1830
|
+
// null/undefined value, the previous resolver skipped the merged
|
|
1831
|
+
// configValue entirely. The round-4 rewrite (6c3ca83e) fixed this via
|
|
1832
|
+
// the `configValue !== SCHEMA_DEFAULT` check — the resolver honors
|
|
1833
|
+
// configValue whenever it differs from the schema default, regardless
|
|
1834
|
+
// of whether raw authored the key.
|
|
1835
|
+
//
|
|
1836
|
+
// This test pins the contract so a future refactor cannot reintroduce
|
|
1837
|
+
// the bug.
|
|
1838
|
+
withIsolatedConnectorsDir(false, () => {
|
|
1839
|
+
// emitLegacyTools: file has null, runtime sets true → honored.
|
|
1840
|
+
assert.equal(
|
|
1841
|
+
parseConfig({ emitLegacyTools: true }, { emitLegacyTools: null }).emitLegacyTools,
|
|
1842
|
+
true,
|
|
1843
|
+
"null raw with runtime true overrides schema default false",
|
|
1844
|
+
);
|
|
1845
|
+
// emitLegacyTools: file has undefined, runtime sets true → honored.
|
|
1846
|
+
assert.equal(
|
|
1847
|
+
parseConfig({ emitLegacyTools: true }, { emitLegacyTools: undefined }).emitLegacyTools,
|
|
1848
|
+
true,
|
|
1849
|
+
"undefined raw with runtime true overrides schema default false",
|
|
1850
|
+
);
|
|
1851
|
+
// emitLegacyTools: file absent (key not in raw), runtime sets true → honored.
|
|
1852
|
+
assert.equal(
|
|
1853
|
+
parseConfig({ emitLegacyTools: true }, {}).emitLegacyTools,
|
|
1854
|
+
true,
|
|
1855
|
+
"absent raw with runtime true overrides schema default false",
|
|
1856
|
+
);
|
|
1857
|
+
// namespaceCatalogEnabled: file has null, runtime sets false → honored
|
|
1858
|
+
// (differs from schema default true).
|
|
1859
|
+
assert.equal(
|
|
1860
|
+
parseConfig({ namespaceCatalogEnabled: false }, { namespaceCatalogEnabled: null }).namespaceCatalogEnabled,
|
|
1861
|
+
false,
|
|
1862
|
+
"null raw with runtime false overrides schema default true",
|
|
1863
|
+
);
|
|
1864
|
+
// Sticky-legacy still works: merged false (schema default), raw empty →
|
|
1865
|
+
// fall through to sticky-legacy.
|
|
1866
|
+
assert.equal(
|
|
1867
|
+
parseConfig({ emitLegacyTools: false }, {}).emitLegacyTools,
|
|
1868
|
+
false,
|
|
1869
|
+
"schema-default false with empty raw falls through to sticky-legacy",
|
|
1870
|
+
);
|
|
1871
|
+
});
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
test("parseConfig schema-default-detection round-4 contract (round 8: runtimeSet gate reverted)", () => {
|
|
1876
|
+
// chatgpt-codex-connector P1 round 8 (PR #1593, src/index.ts:1348):
|
|
1877
|
+
// the round-7 runtimeSet gate was reverted because OpenClaw's loader
|
|
1878
|
+
// runs `applyDefaults: true` before exposing `api.pluginConfig`, so the
|
|
1879
|
+
// set of keys present there cannot reliably distinguish operator-authored
|
|
1880
|
+
// values from schema-default materialization. The resolver now relies
|
|
1881
|
+
// solely on the `configValue !== SCHEMA_DEFAULT` comparison (round-4
|
|
1882
|
+
// contract). This test pins that contract for both gates.
|
|
1883
|
+
//
|
|
1884
|
+
// The third arg `runtimeSet` is kept in the parseConfig signature for
|
|
1885
|
+
// API stability but no longer changes resolver behavior.
|
|
1886
|
+
withIsolatedConnectorsDir(true, () => {
|
|
1887
|
+
// Schema-default false with empty raw: the resolver falls through to
|
|
1888
|
+
// sticky-legacy (the round-4 contract). On an upgraded install, that
|
|
1889
|
+
// means true.
|
|
1890
|
+
assert.equal(
|
|
1891
|
+
parseConfig({ emitLegacyTools: false }, {}, new Set()).emitLegacyTools,
|
|
1892
|
+
true,
|
|
1893
|
+
"schema-default false with empty raw falls through to sticky-legacy (round-4 contract)",
|
|
1894
|
+
);
|
|
1895
|
+
// For namespaceCatalogEnabled: schema default is true. Runtime false
|
|
1896
|
+
// differs from default → honored as runtime intent regardless of
|
|
1897
|
+
// runtimeSet.
|
|
1898
|
+
assert.equal(
|
|
1899
|
+
parseConfig(
|
|
1900
|
+
{ namespaceCatalogEnabled: false },
|
|
1901
|
+
{},
|
|
1902
|
+
new Set(),
|
|
1903
|
+
).namespaceCatalogEnabled,
|
|
1904
|
+
false,
|
|
1905
|
+
"merged false (≠ schema default true) is runtime intent regardless of runtimeSet",
|
|
1906
|
+
);
|
|
1907
|
+
});
|
|
1908
|
+
withIsolatedConnectorsDir(false, () => {
|
|
1909
|
+
// Fresh install: same precedence.
|
|
1910
|
+
assert.equal(
|
|
1911
|
+
parseConfig({ emitLegacyTools: false }, {}, new Set()).emitLegacyTools,
|
|
1912
|
+
false,
|
|
1913
|
+
"fresh install, schema-default false with empty raw returns false",
|
|
1914
|
+
);
|
|
1915
|
+
// Runtime value differs from schema default: honored.
|
|
1916
|
+
assert.equal(
|
|
1917
|
+
parseConfig({ emitLegacyTools: true }, {}, new Set()).emitLegacyTools,
|
|
1918
|
+
true,
|
|
1919
|
+
"runtime true (≠ schema default false) honored",
|
|
1920
|
+
);
|
|
1921
|
+
});
|
|
1922
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -39,6 +39,11 @@ import { expandTildePath } from "./utils/path.js";
|
|
|
39
39
|
// lives in connectors/coerce.ts (a tiny, dependency-free module) so neither
|
|
40
40
|
// config.ts → connectors/index.ts nor the reverse circular import arises.
|
|
41
41
|
import { coerceBool, coerceInstallExtension, coerceNumber } from "./connectors/coerce.js";
|
|
42
|
+
import { hasLegacyConnectorEntries } from "./connectors/paths.js";
|
|
43
|
+
import {
|
|
44
|
+
resolveEmitLegacyTools,
|
|
45
|
+
resolveNamespaceCatalogEnabled,
|
|
46
|
+
} from "./emit-legacy-tools.js";
|
|
42
47
|
import { parseWearablesConfig } from "./wearables/config.js";
|
|
43
48
|
import { parseCodingKnowledgeConfig } from "./coding/coding-knowledge-config.js";
|
|
44
49
|
const DEFAULT_MEMORY_DIR = path.join(
|
|
@@ -613,59 +618,6 @@ function coerceBooleanLike(value: unknown): boolean | undefined {
|
|
|
613
618
|
return undefined;
|
|
614
619
|
}
|
|
615
620
|
|
|
616
|
-
/**
|
|
617
|
-
* Resolve the `emitLegacyTools` opt-out (issue #1427): config field wins, then
|
|
618
|
-
* the REMNIC_/ENGRAM_ env var, then default true. A *present-but-malformed*
|
|
619
|
-
* value fails fast rather than silently re-enabling legacy aliases — this knob
|
|
620
|
-
* controls the advertised MCP `tools/list` surface, so a typo like
|
|
621
|
-
* `emitLegacyTools=fales` must not be misread as `true` (gotcha #51).
|
|
622
|
-
*/
|
|
623
|
-
function resolveEmitLegacyTools(configValue: unknown): boolean {
|
|
624
|
-
const ACCEPTED = "true/false/1/0/yes/no/on/off";
|
|
625
|
-
if (configValue !== undefined && configValue !== null) {
|
|
626
|
-
const coerced = coerceBooleanLike(configValue);
|
|
627
|
-
if (coerced === undefined) {
|
|
628
|
-
throw new Error(
|
|
629
|
-
`emitLegacyTools must be a boolean-like value (${ACCEPTED}); got ${JSON.stringify(configValue)}`,
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
return coerced;
|
|
633
|
-
}
|
|
634
|
-
const envRaw =
|
|
635
|
-
readEnvVar("REMNIC_EMIT_LEGACY_TOOLS") ?? readEnvVar("ENGRAM_EMIT_LEGACY_TOOLS");
|
|
636
|
-
if (envRaw !== undefined) {
|
|
637
|
-
const coerced = coerceBooleanLike(envRaw);
|
|
638
|
-
if (coerced === undefined) {
|
|
639
|
-
throw new Error(
|
|
640
|
-
`REMNIC_EMIT_LEGACY_TOOLS must be a boolean-like value (${ACCEPTED}); got "${envRaw}"`,
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
return coerced;
|
|
644
|
-
}
|
|
645
|
-
return true;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Resolve the `namespaceCatalogEnabled` opt-out (issue #1499). A boolean-like
|
|
650
|
-
* value ("false"/"0"/"no"/"off" etc.) is honored; a PRESENT but unrecognized
|
|
651
|
-
* value ("flase", 2) is REJECTED rather than silently defaulting to enabled
|
|
652
|
-
* (CLAUDE.md rule #51 — reject invalid input instead of silently defaulting),
|
|
653
|
-
* mirroring `resolveEmitLegacyTools`. Defaults to enabled only when absent.
|
|
654
|
-
*/
|
|
655
|
-
function resolveNamespaceCatalogEnabled(configValue: unknown): boolean {
|
|
656
|
-
const ACCEPTED = "true/false/1/0/yes/no/on/off";
|
|
657
|
-
if (configValue !== undefined && configValue !== null) {
|
|
658
|
-
const coerced = coerceBooleanLike(configValue);
|
|
659
|
-
if (coerced === undefined) {
|
|
660
|
-
throw new Error(
|
|
661
|
-
`namespaceCatalogEnabled must be a boolean-like value (${ACCEPTED}); got ${JSON.stringify(configValue)}`,
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
return coerced;
|
|
665
|
-
}
|
|
666
|
-
return true;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
621
|
function readNestedConfig(
|
|
670
622
|
cfg: Record<string, unknown>,
|
|
671
623
|
blockName: string,
|
|
@@ -1056,7 +1008,11 @@ function resolveMemoryOsPreset(value: unknown): MemoryOsPresetName | undefined {
|
|
|
1056
1008
|
return MEMORY_OS_PRESET_ALIASES[normalized];
|
|
1057
1009
|
}
|
|
1058
1010
|
|
|
1059
|
-
export function parseConfig(
|
|
1011
|
+
export function parseConfig(
|
|
1012
|
+
raw: unknown,
|
|
1013
|
+
rawOperatorConfig?: Record<string, unknown> | null,
|
|
1014
|
+
runtimeSet?: ReadonlySet<string>,
|
|
1015
|
+
): PluginConfig {
|
|
1060
1016
|
const baseCfg =
|
|
1061
1017
|
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
1062
1018
|
? (raw as Record<string, unknown>)
|
|
@@ -3031,7 +2987,7 @@ export function parseConfig(raw: unknown): PluginConfig {
|
|
|
3031
2987
|
// a PRESENT but unrecognized value ("flase", 2) is REJECTED rather than
|
|
3032
2988
|
// silently defaulting to enabled (CLAUDE.md rule #51); default to enabled
|
|
3033
2989
|
// only when the value is absent.
|
|
3034
|
-
namespaceCatalogEnabled: resolveNamespaceCatalogEnabled(cfg.namespaceCatalogEnabled),
|
|
2990
|
+
namespaceCatalogEnabled: resolveNamespaceCatalogEnabled(cfg.namespaceCatalogEnabled, rawOperatorConfig, runtimeSet),
|
|
3035
2991
|
// NOTE: namespace identifiers are intentionally NOT sanitized here — the
|
|
3036
2992
|
// codebase rejects unsafe namespaces at the point of use (see
|
|
3037
2993
|
// codex-materialize-runner and NamespaceStorageRouter / resolveNamespaceDir),
|
|
@@ -4100,7 +4056,7 @@ export function parseConfig(raw: unknown): PluginConfig {
|
|
|
4100
4056
|
// Legacy MCP tool aliases opt-out (issue #1427). Config field wins; then
|
|
4101
4057
|
// the REMNIC_/ENGRAM_ env var (gotcha #9); default true for back-compat.
|
|
4102
4058
|
// Malformed values fail fast rather than silently defaulting (gotcha #51).
|
|
4103
|
-
emitLegacyTools: resolveEmitLegacyTools(cfg.emitLegacyTools),
|
|
4059
|
+
emitLegacyTools: resolveEmitLegacyTools(cfg.emitLegacyTools, rawOperatorConfig, runtimeSet),
|
|
4104
4060
|
// Codex citation parity (issue #379)
|
|
4105
4061
|
citationsEnabled: cfg.citationsEnabled === true,
|
|
4106
4062
|
citationsAutoDetect: cfg.citationsAutoDetect !== false,
|
package/src/connectors/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { launchProcessSync } from "../runtime/child-process.js";
|
|
|
16
16
|
import { mergeEnv, readEnvVar, resolveHomeDir } from "../runtime/env.js";
|
|
17
17
|
import { expandTildePath } from "../utils/path.js";
|
|
18
18
|
import { coerceInstallExtension } from "./coerce.js";
|
|
19
|
+
import { getConnectorsDir, getRegistryPath } from "./paths.js";
|
|
20
|
+
export { getConnectorsDir, getRegistryPath } from "./paths.js";
|
|
19
21
|
|
|
20
22
|
// Native memory artifact materialization for Codex CLI (#378). Surfaced here
|
|
21
23
|
// so downstream callers can `import { materializeForNamespace } from "@remnic/core/connectors"`.
|
|
@@ -556,7 +558,6 @@ const BUILTIN_CONNECTORS: ConnectorManifest[] = [
|
|
|
556
558
|
|
|
557
559
|
// ── Registry management ───────────────────────────────────────────────────
|
|
558
560
|
|
|
559
|
-
const REGISTRY_DIR_NAME = ".engram-connectors";
|
|
560
561
|
const CONNECTOR_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
561
562
|
|
|
562
563
|
function isValidConnectorId(connectorId: unknown): connectorId is string {
|
|
@@ -572,13 +573,6 @@ function isConnectorManifest(value: unknown): value is ConnectorManifest {
|
|
|
572
573
|
);
|
|
573
574
|
}
|
|
574
575
|
|
|
575
|
-
export function getRegistryPath(): string {
|
|
576
|
-
const xdgConfigHome = readEnvVar("XDG_CONFIG_HOME");
|
|
577
|
-
const configDir = xdgConfigHome
|
|
578
|
-
? path.join(xdgConfigHome, "engram")
|
|
579
|
-
: path.join(resolveHomeDir(), ".config", "engram");
|
|
580
|
-
return path.join(configDir, REGISTRY_DIR_NAME, "registry.json");
|
|
581
|
-
}
|
|
582
576
|
|
|
583
577
|
export function loadRegistry(): ConnectorRegistry {
|
|
584
578
|
const regPath = getRegistryPath();
|
|
@@ -3058,13 +3052,6 @@ export function removeCodexMemoryExtension(
|
|
|
3058
3052
|
|
|
3059
3053
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
3060
3054
|
|
|
3061
|
-
function getConnectorsDir(): string {
|
|
3062
|
-
const xdgConfigHome = readEnvVar("XDG_CONFIG_HOME");
|
|
3063
|
-
const configDir = xdgConfigHome
|
|
3064
|
-
? path.join(xdgConfigHome, "engram")
|
|
3065
|
-
: path.join(resolveHomeDir(), ".config", "engram");
|
|
3066
|
-
return path.join(configDir, REGISTRY_DIR_NAME, "connectors");
|
|
3067
|
-
}
|
|
3068
3055
|
|
|
3069
3056
|
// ── WeClone proxy config helpers ───────────────────────────────────────────
|
|
3070
3057
|
//
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { readEnvVar, resolveHomeDir } from "../runtime/env.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Connector registry directory name. Kept under the legacy `engram` config
|
|
8
|
+
* root for backward compatibility with existing installs; the rename to a
|
|
9
|
+
* `remnic` path (with a legacy read-fallback) is tracked in #1518.
|
|
10
|
+
*/
|
|
11
|
+
export const REGISTRY_DIR_NAME = ".engram-connectors";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Single source of truth for the connectors config root
|
|
15
|
+
* (`$XDG_CONFIG_HOME/engram` or `~/.config/engram`). Issue #1527 flagged this
|
|
16
|
+
* derivation as previously duplicated across call sites — add new callers
|
|
17
|
+
* here, never re-derive the path inline.
|
|
18
|
+
*/
|
|
19
|
+
export function getConnectorsConfigRoot(): string {
|
|
20
|
+
const xdgConfigHome = readEnvVar("XDG_CONFIG_HOME");
|
|
21
|
+
return xdgConfigHome
|
|
22
|
+
? path.join(xdgConfigHome, "engram")
|
|
23
|
+
: path.join(resolveHomeDir(), ".config", "engram");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Path of the connector registry manifest file. */
|
|
27
|
+
export function getRegistryPath(): string {
|
|
28
|
+
return path.join(getConnectorsConfigRoot(), REGISTRY_DIR_NAME, "registry.json");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Directory holding one `<connector-id>.json` per installed connector. */
|
|
32
|
+
export function getConnectorsDir(): string {
|
|
33
|
+
return path.join(getConnectorsConfigRoot(), REGISTRY_DIR_NAME, "connectors");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sticky-legacy evidence for `emitLegacyTools` (issue #1550): any persisted
|
|
38
|
+
* connector entry under the legacy engram connectors dir means an existing
|
|
39
|
+
* install whose clients may still address engram_* aliases. Missing or
|
|
40
|
+
* unreadable dir means fresh install — no evidence, no aliases.
|
|
41
|
+
*/
|
|
42
|
+
export function hasLegacyConnectorEntries(): boolean {
|
|
43
|
+
try {
|
|
44
|
+
return fs
|
|
45
|
+
.readdirSync(getConnectorsDir())
|
|
46
|
+
.some((name) => name.endsWith(".json"));
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|