@nexrall/code-core 1.4.77 → 1.4.78
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/agent/effortCap.d.ts +24 -0
- package/dist/agent/effortCap.d.ts.map +1 -0
- package/dist/agent/effortCap.js +66 -0
- package/dist/agent/hooks.d.ts +10 -2
- package/dist/agent/hooks.d.ts.map +1 -1
- package/dist/agent/hooks.js +9 -3
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +14 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/mcp/manager.d.ts +8 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +64 -57
- package/dist/permissions/destructiveTokens.d.ts +1 -0
- package/dist/permissions/destructiveTokens.d.ts.map +1 -1
- package/dist/permissions/destructiveTokens.js +93 -0
- package/dist/permissions/rules.d.ts +12 -0
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +89 -7
- package/dist/plugins/configStore.d.ts +43 -0
- package/dist/plugins/configStore.d.ts.map +1 -0
- package/dist/plugins/configStore.js +253 -0
- package/dist/plugins/files.d.ts +10 -0
- package/dist/plugins/files.d.ts.map +1 -0
- package/dist/plugins/files.js +85 -0
- package/dist/plugins/index.d.ts +3 -22
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +30 -40
- package/dist/plugins/resolve.d.ts +18 -0
- package/dist/plugins/resolve.d.ts.map +1 -0
- package/dist/plugins/resolve.js +152 -0
- package/dist/plugins/userConfig.d.ts +90 -0
- package/dist/plugins/userConfig.d.ts.map +1 -0
- package/dist/plugins/userConfig.js +332 -0
- package/dist/plugins/validate.d.ts +14 -0
- package/dist/plugins/validate.d.ts.map +1 -0
- package/dist/plugins/validate.js +222 -0
- package/package.json +1 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Plugin `userConfig` (S24, Claude Code parity — G24) ─────────────────────
|
|
3
|
+
//
|
|
4
|
+
// A plugin that needs a value from the user (an API endpoint, a token, a log
|
|
5
|
+
// directory) declares it in plugin.json instead of asking the user to edit
|
|
6
|
+
// settings by hand:
|
|
7
|
+
//
|
|
8
|
+
// "userConfig": {
|
|
9
|
+
// "api_token": { "type": "string", "title": "API token",
|
|
10
|
+
// "description": "…", "sensitive": true }
|
|
11
|
+
// }
|
|
12
|
+
//
|
|
13
|
+
// Values are resolved by configStore.ts (settings `pluginConfigs` → secrets
|
|
14
|
+
// file → manifest default) and reach the plugin three ways, matching Claude
|
|
15
|
+
// Code's model:
|
|
16
|
+
//
|
|
17
|
+
// • `${user_config.KEY}` substitution — MCP server configs (sensitive values
|
|
18
|
+
// included) and skill bodies (non-sensitive only; a sensitive reference is
|
|
19
|
+
// left as the literal token rather than shipped into model context);
|
|
20
|
+
// • `NEXRALL_PLUGIN_OPTION_<KEY>` environment variables on every hook
|
|
21
|
+
// process the plugin ships (KEY uppercased) — the ONLY way a shell-form
|
|
22
|
+
// hook can read a value, because a shell-form `command` rejects
|
|
23
|
+
// `${user_config.*}` (the substituted value would be re-parsed by the
|
|
24
|
+
// shell; validate.ts reports the reference as an error);
|
|
25
|
+
// • the `nex plugin configure` command and the install-time
|
|
26
|
+
// `--config key=value` flag, which write values back.
|
|
27
|
+
//
|
|
28
|
+
// This module is PURE (no fs): schema parsing, value validation, substitution
|
|
29
|
+
// and env naming. The store lives in configStore.ts.
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.USER_CONFIG_TYPES = void 0;
|
|
32
|
+
exports.parseUserConfig = parseUserConfig;
|
|
33
|
+
exports.validateUserConfigValue = validateUserConfigValue;
|
|
34
|
+
exports.coerceUserConfigValue = coerceUserConfigValue;
|
|
35
|
+
exports.findUserConfigRefs = findUserConfigRefs;
|
|
36
|
+
exports.substituteUserConfig = substituteUserConfig;
|
|
37
|
+
exports.substituteUserConfigDeep = substituteUserConfigDeep;
|
|
38
|
+
exports.collectUserConfigRefsDeep = collectUserConfigRefsDeep;
|
|
39
|
+
exports.pluginOptionEnvKey = pluginOptionEnvKey;
|
|
40
|
+
exports.pluginOptionEnvMap = pluginOptionEnvMap;
|
|
41
|
+
/** Supported value types, matching Claude Code's set. */
|
|
42
|
+
exports.USER_CONFIG_TYPES = ['string', 'number', 'boolean', 'directory', 'file'];
|
|
43
|
+
const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
44
|
+
/** Fields an option object may carry. Anything else is an error (strict object). */
|
|
45
|
+
const KNOWN_FIELDS = new Set([
|
|
46
|
+
'type',
|
|
47
|
+
'title',
|
|
48
|
+
'description',
|
|
49
|
+
'required',
|
|
50
|
+
'default',
|
|
51
|
+
'options',
|
|
52
|
+
'multiple',
|
|
53
|
+
'sensitive',
|
|
54
|
+
'min',
|
|
55
|
+
'max',
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* Parse the manifest's `userConfig` block strictly — Claude Code's rule: an
|
|
59
|
+
* unknown key inside an option is an error. One bad option invalidates the
|
|
60
|
+
* WHOLE block (all-or-nothing): a plugin half-configured by a typo would be
|
|
61
|
+
* worse than one that loudly needs its manifest fixed.
|
|
62
|
+
*/
|
|
63
|
+
function parseUserConfig(manifest) {
|
|
64
|
+
const raw = manifest?.userConfig;
|
|
65
|
+
if (raw === undefined)
|
|
66
|
+
return { options: [], errors: [] };
|
|
67
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
68
|
+
return { options: [], errors: ['userConfig must be an object of option definitions'] };
|
|
69
|
+
}
|
|
70
|
+
const options = [];
|
|
71
|
+
const errors = [];
|
|
72
|
+
for (const [key, def] of Object.entries(raw)) {
|
|
73
|
+
const problems = parseOne(key, def);
|
|
74
|
+
if (typeof problems === 'string') {
|
|
75
|
+
errors.push(problems);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
options.push(problems);
|
|
79
|
+
}
|
|
80
|
+
if (errors.length)
|
|
81
|
+
return { options: [], errors };
|
|
82
|
+
return { options, errors };
|
|
83
|
+
}
|
|
84
|
+
/** Parse one option; returns the option or its first error. */
|
|
85
|
+
function parseOne(key, def) {
|
|
86
|
+
if (!KEY_RE.test(key))
|
|
87
|
+
return `userConfig "${key}": key must be letters, digits and underscores, not starting with a digit`;
|
|
88
|
+
if (!def || typeof def !== 'object' || Array.isArray(def))
|
|
89
|
+
return `userConfig "${key}": must be an object`;
|
|
90
|
+
const d = def;
|
|
91
|
+
for (const field of Object.keys(d)) {
|
|
92
|
+
if (!KNOWN_FIELDS.has(field))
|
|
93
|
+
return `userConfig "${key}": unknown field "${field}" (allowed: ${[...KNOWN_FIELDS].join(', ')})`;
|
|
94
|
+
}
|
|
95
|
+
const type = typeof d.type === 'string' ? d.type : undefined;
|
|
96
|
+
if (!type || !exports.USER_CONFIG_TYPES.includes(type))
|
|
97
|
+
return `userConfig "${key}": type must be ${exports.USER_CONFIG_TYPES.join(' | ')}`;
|
|
98
|
+
if (typeof d.title !== 'string' || !d.title.trim())
|
|
99
|
+
return `userConfig "${key}": title is required`;
|
|
100
|
+
if (typeof d.description !== 'string' || !d.description.trim())
|
|
101
|
+
return `userConfig "${key}": description is required`;
|
|
102
|
+
for (const boolField of ['required', 'multiple', 'sensitive']) {
|
|
103
|
+
if (d[boolField] !== undefined && typeof d[boolField] !== 'boolean')
|
|
104
|
+
return `userConfig "${key}": ${boolField} must be a boolean`;
|
|
105
|
+
}
|
|
106
|
+
if (d.options !== undefined) {
|
|
107
|
+
if (type !== 'string')
|
|
108
|
+
return `userConfig "${key}": options applies to string fields only (this is ${type})`;
|
|
109
|
+
if (d.multiple === true)
|
|
110
|
+
return `userConfig "${key}": options cannot be combined with multiple`;
|
|
111
|
+
if (d.sensitive === true)
|
|
112
|
+
return `userConfig "${key}": options cannot be combined with sensitive`;
|
|
113
|
+
if (!Array.isArray(d.options) || !d.options.length)
|
|
114
|
+
return `userConfig "${key}": options must be a non-empty array`;
|
|
115
|
+
for (const o of d.options) {
|
|
116
|
+
if (typeof o !== 'string' || !o.trim() || o.length > 64 || o.length < 1)
|
|
117
|
+
return `userConfig "${key}": each option must be a plain label of 1-64 characters`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (d.multiple === true && type !== 'string')
|
|
121
|
+
return `userConfig "${key}": multiple applies to string fields only`;
|
|
122
|
+
if (d.min !== undefined || d.max !== undefined) {
|
|
123
|
+
if (type !== 'number')
|
|
124
|
+
return `userConfig "${key}": min/max apply to number fields only`;
|
|
125
|
+
if (d.min !== undefined && typeof d.min !== 'number')
|
|
126
|
+
return `userConfig "${key}": min must be a number`;
|
|
127
|
+
if (d.max !== undefined && typeof d.max !== 'number')
|
|
128
|
+
return `userConfig "${key}": max must be a number`;
|
|
129
|
+
if (typeof d.min === 'number' && typeof d.max === 'number' && d.min > d.max)
|
|
130
|
+
return `userConfig "${key}": min is greater than max`;
|
|
131
|
+
}
|
|
132
|
+
const option = {
|
|
133
|
+
key,
|
|
134
|
+
type,
|
|
135
|
+
title: d.title,
|
|
136
|
+
description: d.description,
|
|
137
|
+
...(d.required === true ? { required: true } : {}),
|
|
138
|
+
...(d.multiple === true ? { multiple: true } : {}),
|
|
139
|
+
...(d.sensitive === true ? { sensitive: true } : {}),
|
|
140
|
+
...(Array.isArray(d.options) ? { options: [...d.options] } : {}),
|
|
141
|
+
...(typeof d.min === 'number' ? { min: d.min } : {}),
|
|
142
|
+
...(typeof d.max === 'number' ? { max: d.max } : {}),
|
|
143
|
+
};
|
|
144
|
+
if (d.default !== undefined) {
|
|
145
|
+
const bad = validateUserConfigValue(option, d.default);
|
|
146
|
+
if (bad)
|
|
147
|
+
return `userConfig "${key}": default is invalid — ${bad}`;
|
|
148
|
+
option.default = d.default;
|
|
149
|
+
}
|
|
150
|
+
return option;
|
|
151
|
+
}
|
|
152
|
+
/** Validate one value against its option. Returns an error string, or null when valid. */
|
|
153
|
+
function validateUserConfigValue(option, value) {
|
|
154
|
+
if (option.multiple) {
|
|
155
|
+
if (!Array.isArray(value))
|
|
156
|
+
return 'expected an array of strings (multiple)';
|
|
157
|
+
for (const v of value) {
|
|
158
|
+
if (typeof v !== 'string')
|
|
159
|
+
return 'every entry must be a string';
|
|
160
|
+
const bad = validateSingle(option, v);
|
|
161
|
+
if (bad)
|
|
162
|
+
return bad;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return validateSingle(option, value);
|
|
167
|
+
}
|
|
168
|
+
function validateSingle(option, value) {
|
|
169
|
+
switch (option.type) {
|
|
170
|
+
case 'string':
|
|
171
|
+
case 'directory':
|
|
172
|
+
case 'file':
|
|
173
|
+
if (typeof value !== 'string')
|
|
174
|
+
return `expected a ${option.type}`;
|
|
175
|
+
if (option.options && !option.options.includes(value))
|
|
176
|
+
return `must be one of: ${option.options.join(', ')}`;
|
|
177
|
+
return null;
|
|
178
|
+
case 'number': {
|
|
179
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
180
|
+
return 'expected a number';
|
|
181
|
+
if (option.min !== undefined && value < option.min)
|
|
182
|
+
return `must be at least ${option.min}`;
|
|
183
|
+
if (option.max !== undefined && value > option.max)
|
|
184
|
+
return `must be at most ${option.max}`;
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
case 'boolean':
|
|
188
|
+
return typeof value === 'boolean' ? null : 'expected true or false';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Coerce a CLI-supplied STRING into the option's type (values arrive from
|
|
193
|
+
* `--config key=value` / `--values-stdin` where everything is text).
|
|
194
|
+
* `parseNumber` is strict: "12abc" must not become 12.
|
|
195
|
+
*/
|
|
196
|
+
function coerceUserConfigValue(option, raw) {
|
|
197
|
+
if (option.multiple) {
|
|
198
|
+
// JSON array, or comma-separated.
|
|
199
|
+
const text = raw.trim();
|
|
200
|
+
let arr;
|
|
201
|
+
if (text.startsWith('[')) {
|
|
202
|
+
try {
|
|
203
|
+
arr = JSON.parse(text);
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
return { ok: false, error: `could not parse as a JSON array: ${e.message}` };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
arr = text ? text.split(',').map((s) => s.trim()) : [];
|
|
211
|
+
}
|
|
212
|
+
const bad = validateUserConfigValue(option, arr);
|
|
213
|
+
return bad ? { ok: false, error: bad } : { ok: true, value: arr };
|
|
214
|
+
}
|
|
215
|
+
switch (option.type) {
|
|
216
|
+
case 'string':
|
|
217
|
+
case 'directory':
|
|
218
|
+
case 'file': {
|
|
219
|
+
const bad = validateUserConfigValue(option, raw);
|
|
220
|
+
return bad ? { ok: false, error: bad } : { ok: true, value: raw };
|
|
221
|
+
}
|
|
222
|
+
case 'number': {
|
|
223
|
+
const t = raw.trim();
|
|
224
|
+
const n = t === '' ? NaN : Number(t);
|
|
225
|
+
if (!Number.isFinite(n))
|
|
226
|
+
return { ok: false, error: `"${raw}" is not a number` };
|
|
227
|
+
const bad = validateUserConfigValue(option, n);
|
|
228
|
+
return bad ? { ok: false, error: bad } : { ok: true, value: n };
|
|
229
|
+
}
|
|
230
|
+
case 'boolean': {
|
|
231
|
+
const t = raw.trim().toLowerCase();
|
|
232
|
+
if (t === 'true' || t === '1' || t === 'yes')
|
|
233
|
+
return { ok: true, value: true };
|
|
234
|
+
if (t === 'false' || t === '0' || t === 'no')
|
|
235
|
+
return { ok: true, value: false };
|
|
236
|
+
return { ok: false, error: `"${raw}" is not a boolean (true/false)` };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ─── Substitution ────────────────────────────────────────────────────────────
|
|
241
|
+
const REF_RE = /\$\{user_config\.([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
242
|
+
/** Every `${user_config.KEY}` reference in a string, unique and in first-seen order. */
|
|
243
|
+
function findUserConfigRefs(text) {
|
|
244
|
+
const out = new Set();
|
|
245
|
+
for (const m of text.matchAll(REF_RE))
|
|
246
|
+
out.add(m[1]);
|
|
247
|
+
return [...out];
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Replace `${user_config.KEY}` with resolved values. A reference with no value
|
|
251
|
+
* keeps its literal token — a visible failure the plugin's author can debug,
|
|
252
|
+
* never an empty string masquerading as a value. `includeSensitive: false`
|
|
253
|
+
* (skill bodies) leaves sensitive tokens in place; configStore decides which
|
|
254
|
+
* values may be exposed where.
|
|
255
|
+
*/
|
|
256
|
+
function substituteUserConfig(text, opts) {
|
|
257
|
+
const result = { text, substituted: [], skippedSensitive: [], unresolved: [] };
|
|
258
|
+
const byKey = new Map(opts.options.map((o) => [o.key, o]));
|
|
259
|
+
result.text = text.replace(REF_RE, (whole, key) => {
|
|
260
|
+
const option = byKey.get(key);
|
|
261
|
+
if (!option) {
|
|
262
|
+
result.unresolved.push(key);
|
|
263
|
+
return whole; // undeclared reference: validate.ts reports it; runtime keeps it visible
|
|
264
|
+
}
|
|
265
|
+
if (option.sensitive && !opts.includeSensitive) {
|
|
266
|
+
result.skippedSensitive.push(key);
|
|
267
|
+
return whole;
|
|
268
|
+
}
|
|
269
|
+
const v = opts.values[key];
|
|
270
|
+
if (v === undefined) {
|
|
271
|
+
result.unresolved.push(key);
|
|
272
|
+
return whole;
|
|
273
|
+
}
|
|
274
|
+
if (!result.substituted.includes(key))
|
|
275
|
+
result.substituted.push(key);
|
|
276
|
+
return Array.isArray(v) ? v.join(', ') : String(v);
|
|
277
|
+
});
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
/** Deep substitution over an object/array graph (MCP server configs). Strings only. */
|
|
281
|
+
function substituteUserConfigDeep(value, opts) {
|
|
282
|
+
if (typeof value === 'string')
|
|
283
|
+
return substituteUserConfig(value, opts).text;
|
|
284
|
+
if (Array.isArray(value))
|
|
285
|
+
return value.map((v) => substituteUserConfigDeep(v, opts));
|
|
286
|
+
if (value && typeof value === 'object') {
|
|
287
|
+
const out = {};
|
|
288
|
+
for (const [k, v] of Object.entries(value)) {
|
|
289
|
+
out[k] = substituteUserConfigDeep(v, opts);
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
/** All `${user_config.KEY}` refs found anywhere in a JSON value graph, unique. */
|
|
296
|
+
function collectUserConfigRefsDeep(value) {
|
|
297
|
+
const out = new Set();
|
|
298
|
+
const walk = (v) => {
|
|
299
|
+
if (typeof v === 'string')
|
|
300
|
+
for (const k of findUserConfigRefs(v))
|
|
301
|
+
out.add(k);
|
|
302
|
+
else if (Array.isArray(v))
|
|
303
|
+
v.forEach(walk);
|
|
304
|
+
else if (v && typeof v === 'object')
|
|
305
|
+
Object.values(v).forEach(walk);
|
|
306
|
+
};
|
|
307
|
+
walk(value);
|
|
308
|
+
return [...out];
|
|
309
|
+
}
|
|
310
|
+
// ─── Environment ─────────────────────────────────────────────────────────────
|
|
311
|
+
/** Claude Code's naming rule: `CLAUDE_PLUGIN_OPTION_<KEY>` → ours mirrors it. */
|
|
312
|
+
function pluginOptionEnvKey(key) {
|
|
313
|
+
return `NEXRALL_PLUGIN_OPTION_${key.toUpperCase()}`;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The env map a plugin's hook processes receive. Only scalar values can ride
|
|
317
|
+
* an environment variable; an array/object value is skipped (a shell would see
|
|
318
|
+
* "[object Object]" — worse than absent).
|
|
319
|
+
*/
|
|
320
|
+
function pluginOptionEnvMap(options, values) {
|
|
321
|
+
const env = {};
|
|
322
|
+
for (const o of options) {
|
|
323
|
+
const v = values[o.key];
|
|
324
|
+
if (v === undefined)
|
|
325
|
+
continue;
|
|
326
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
327
|
+
env[pluginOptionEnvKey(o.key)] = String(v);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return env;
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=userConfig.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type UserConfigOption } from './userConfig';
|
|
2
|
+
export interface PluginValidateReport {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
dir: string;
|
|
5
|
+
/** Which manifest file was found (null when none). */
|
|
6
|
+
manifest: string | null;
|
|
7
|
+
userConfig: UserConfigOption[];
|
|
8
|
+
errors: string[];
|
|
9
|
+
warnings: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function validatePluginDir(dir: string): PluginValidateReport;
|
|
12
|
+
/** One-line human summary lines for the CLI printer (warnings then errors). */
|
|
13
|
+
export declare function formatValidateReport(r: PluginValidateReport): string[];
|
|
14
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/plugins/validate.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAkE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAsBD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,CA6HnE;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,oBAAoB,GAAG,MAAM,EAAE,CAUtE"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── `nex plugin validate` (S24) ─────────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// One structural check of a plugin directory, runnable in CI before anyone
|
|
5
|
+
// installs it. It answers the questions the install-time inspector cannot:
|
|
6
|
+
//
|
|
7
|
+
// • does the manifest parse, and is its `userConfig` block legal (strict —
|
|
8
|
+
// an unknown field inside an option is an error, matching Claude Code)?
|
|
9
|
+
// • do hooks.json / mcp.json parse, and resolve through the SAME file table
|
|
10
|
+
// the runtime loader reads (a component under a path the loader ignores
|
|
11
|
+
// would otherwise pass validation and do nothing);
|
|
12
|
+
// • does every `${user_config.KEY}` reference name a DECLARED option
|
|
13
|
+
// (Claude Code 2.1.281's check) — and is it used in a place that accepts
|
|
14
|
+
// it? A shell-form hook `command` never does: the substituted value would
|
|
15
|
+
// be re-parsed by the shell, so the runtime rejects that reference and the
|
|
16
|
+
// validate report says to read NEXRALL_PLUGIN_OPTION_<KEY> from the
|
|
17
|
+
// environment instead;
|
|
18
|
+
// • does every MCP server entry carry what a connection needs (a string
|
|
19
|
+
// `command`, or a string `url`)?
|
|
20
|
+
// • insecure remote URLs (plain http:// to a non-localhost host) as warnings.
|
|
21
|
+
//
|
|
22
|
+
// Pure reads; no plugin code ever executes here.
|
|
23
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
26
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
27
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(o, k2, desc);
|
|
30
|
+
}) : (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
}));
|
|
34
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
35
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
36
|
+
}) : function(o, v) {
|
|
37
|
+
o["default"] = v;
|
|
38
|
+
});
|
|
39
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
40
|
+
var ownKeys = function(o) {
|
|
41
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
42
|
+
var ar = [];
|
|
43
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
44
|
+
return ar;
|
|
45
|
+
};
|
|
46
|
+
return ownKeys(o);
|
|
47
|
+
};
|
|
48
|
+
return function (mod) {
|
|
49
|
+
if (mod && mod.__esModule) return mod;
|
|
50
|
+
var result = {};
|
|
51
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
52
|
+
__setModuleDefault(result, mod);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.validatePluginDir = validatePluginDir;
|
|
58
|
+
exports.formatValidateReport = formatValidateReport;
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
const files_1 = require("./files");
|
|
62
|
+
const userConfig_1 = require("./userConfig");
|
|
63
|
+
const LOCALHOST = /^(localhost|127\.0\.0\.1|\[::1\]|::1)$/i;
|
|
64
|
+
function insecureUrl(url) {
|
|
65
|
+
if (!/^http:\/\//i.test(url))
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
const host = new URL(url).hostname;
|
|
69
|
+
return !LOCALHOST.test(host);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return true; // unparseable http URL: still the insecure scheme
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Walk one hooks phase entry (flat and nested CC forms) into handler objects. */
|
|
76
|
+
function hookHandlers(entry) {
|
|
77
|
+
if (!entry || typeof entry !== 'object')
|
|
78
|
+
return [];
|
|
79
|
+
const e = entry;
|
|
80
|
+
if (Array.isArray(e.hooks))
|
|
81
|
+
return e.hooks.filter((h) => !!h && typeof h === 'object');
|
|
82
|
+
return [e];
|
|
83
|
+
}
|
|
84
|
+
function validatePluginDir(dir) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
const warnings = [];
|
|
87
|
+
const report = { ok: true, dir, manifest: null, userConfig: [], errors, warnings };
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.statSync(dir).isDirectory()) {
|
|
90
|
+
errors.push(`not a directory: ${dir}`);
|
|
91
|
+
return { ...report, ok: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
errors.push(`does not exist: ${dir}`);
|
|
96
|
+
return { ...report, ok: false };
|
|
97
|
+
}
|
|
98
|
+
// ── manifest ───────────────────────────────────────────────────────────────
|
|
99
|
+
const manifestFile = (0, files_1.resolvePluginFile)(dir, 'manifest');
|
|
100
|
+
report.manifest = manifestFile;
|
|
101
|
+
let manifest = null;
|
|
102
|
+
if (!manifestFile) {
|
|
103
|
+
warnings.push('no plugin.json found (the plugin still loads by directory name, but cannot declare userConfig)');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
try {
|
|
107
|
+
const j = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
|
|
108
|
+
if (!j || typeof j !== 'object' || Array.isArray(j)) {
|
|
109
|
+
errors.push(`${manifestFile}: must be a JSON object`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
manifest = j;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
errors.push(`${manifestFile}: ${e.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (manifest) {
|
|
120
|
+
for (const field of ['name', 'version', 'description']) {
|
|
121
|
+
if (manifest[field] !== undefined && typeof manifest[field] !== 'string') {
|
|
122
|
+
errors.push(`plugin.json: "${field}" must be a string`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (manifest.userConfig !== undefined) {
|
|
126
|
+
const parsed = (0, userConfig_1.parseUserConfig)(manifest);
|
|
127
|
+
report.userConfig = parsed.options;
|
|
128
|
+
errors.push(...parsed.errors);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const declared = new Set(report.userConfig.map((o) => o.key));
|
|
132
|
+
// ── hooks.json ─────────────────────────────────────────────────────────────
|
|
133
|
+
const hooksFile = (0, files_1.resolvePluginFile)(dir, 'hooks');
|
|
134
|
+
if (hooksFile) {
|
|
135
|
+
try {
|
|
136
|
+
const j = JSON.parse(fs.readFileSync(hooksFile, 'utf-8'));
|
|
137
|
+
const hooks = (j?.hooks ?? j);
|
|
138
|
+
if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) {
|
|
139
|
+
errors.push(`${hooksFile}: must be an object of hook phases`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
for (const [phase, entries] of Object.entries(hooks)) {
|
|
143
|
+
if (!Array.isArray(entries)) {
|
|
144
|
+
errors.push(`${hooksFile}: "${phase}" must be an array`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
for (const handler of entries.flatMap(hookHandlers)) {
|
|
148
|
+
const type = handler.type ?? (handler.url ? 'http' : handler.prompt ? 'prompt' : 'command');
|
|
149
|
+
if (type === 'command') {
|
|
150
|
+
const cmd = handler.command;
|
|
151
|
+
if (typeof cmd !== 'string' || !cmd.trim()) {
|
|
152
|
+
errors.push(`${hooksFile}: a command hook in "${phase}" has no command string`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if ((0, userConfig_1.findUserConfigRefs)(cmd).length) {
|
|
156
|
+
errors.push(`${hooksFile}: a command hook in "${phase}" uses \${user_config.*} in its shell-form command — ` +
|
|
157
|
+
'the value would be re-parsed by the shell. Read NEXRALL_PLUGIN_OPTION_<KEY> from the hook environment instead');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
errors.push(`${hooksFile}: ${e.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ── mcp.json ───────────────────────────────────────────────────────────────
|
|
169
|
+
const mcpFile = (0, files_1.resolvePluginFile)(dir, 'mcp');
|
|
170
|
+
if (mcpFile) {
|
|
171
|
+
try {
|
|
172
|
+
const j = JSON.parse(fs.readFileSync(mcpFile, 'utf-8'));
|
|
173
|
+
const servers = (j?.mcpServers ?? j);
|
|
174
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
|
|
175
|
+
errors.push(`${mcpFile}: must be an object of server configs (or { mcpServers: … })`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
179
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {
|
|
180
|
+
errors.push(`${mcpFile}: server "${name}" is not an object — it will not connect`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const c = cfg;
|
|
184
|
+
const hasCommand = typeof c.command === 'string' && c.command.trim() !== '';
|
|
185
|
+
const hasUrl = typeof c.url === 'string' && c.url.trim() !== '';
|
|
186
|
+
if (!hasCommand && !hasUrl) {
|
|
187
|
+
errors.push(`${mcpFile}: server "${name}" has neither a string "command" nor a "url" — it will not connect`);
|
|
188
|
+
}
|
|
189
|
+
if (hasUrl && insecureUrl(String(c.url))) {
|
|
190
|
+
warnings.push(`${mcpFile}: server "${name}" uses a plain http:// URL (${String(c.url)})`);
|
|
191
|
+
}
|
|
192
|
+
for (const ref of (0, userConfig_1.collectUserConfigRefsDeep)(cfg)) {
|
|
193
|
+
if (!declared.has(ref)) {
|
|
194
|
+
errors.push(`${mcpFile}: server "${name}" references \${user_config.${ref}} but the manifest does not declare "${ref}"`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
errors.push(`${mcpFile}: ${e.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
report.ok = errors.length === 0;
|
|
205
|
+
return report;
|
|
206
|
+
}
|
|
207
|
+
/** One-line human summary lines for the CLI printer (warnings then errors). */
|
|
208
|
+
function formatValidateReport(r) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
const label = path.basename(r.dir) || r.dir;
|
|
211
|
+
lines.push(` Plugin: ${label}`);
|
|
212
|
+
lines.push(` Manifest: ${r.manifest ?? '(none)'}`);
|
|
213
|
+
if (r.userConfig.length)
|
|
214
|
+
lines.push(` userConfig options: ${r.userConfig.map((o) => o.key).join(', ')}`);
|
|
215
|
+
for (const w of r.warnings)
|
|
216
|
+
lines.push(` ⚠ ${w}`);
|
|
217
|
+
for (const e of r.errors)
|
|
218
|
+
lines.push(` ✗ ${e}`);
|
|
219
|
+
lines.push(r.ok ? ' ✓ Validation passed' : ` ✗ Validation failed (${r.errors.length} error(s))`);
|
|
220
|
+
return lines;
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=validate.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.78",
|
|
4
4
|
"description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nexrall <support@nexrall.com> (https://nexrall.com)",
|