@nexrall/code-core 1.0.0
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/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/agent/agentTypes.d.ts +17 -0
- package/dist/agent/agentTypes.d.ts.map +1 -0
- package/dist/agent/agentTypes.js +156 -0
- package/dist/agent/loop.d.ts +3 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +775 -0
- package/dist/api/client.d.ts +27 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +414 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +95 -0
- package/dist/checkpoint/manager.d.ts +86 -0
- package/dist/checkpoint/manager.d.ts.map +1 -0
- package/dist/checkpoint/manager.js +409 -0
- package/dist/commands/loader.d.ts +18 -0
- package/dist/commands/loader.d.ts.map +1 -0
- package/dist/commands/loader.js +183 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/mcp/client.d.ts +46 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +128 -0
- package/dist/mcp/httpClient.d.ts +25 -0
- package/dist/mcp/httpClient.d.ts.map +1 -0
- package/dist/mcp/httpClient.js +143 -0
- package/dist/mcp/manager.d.ts +56 -0
- package/dist/mcp/manager.d.ts.map +1 -0
- package/dist/mcp/manager.js +234 -0
- package/dist/mcp/sseClient.d.ts +30 -0
- package/dist/mcp/sseClient.d.ts.map +1 -0
- package/dist/mcp/sseClient.js +224 -0
- package/dist/permissions/rules.d.ts +20 -0
- package/dist/permissions/rules.d.ts.map +1 -0
- package/dist/permissions/rules.js +218 -0
- package/dist/plugins/index.d.ts +18 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +139 -0
- package/dist/tools/executor.d.ts +6 -0
- package/dist/tools/executor.d.ts.map +1 -0
- package/dist/tools/executor.js +1458 -0
- package/dist/tools/sandbox.d.ts +13 -0
- package/dist/tools/sandbox.d.ts.map +1 -0
- package/dist/tools/sandbox.js +140 -0
- package/dist/tools/symbols.d.ts +13 -0
- package/dist/tools/symbols.d.ts.map +1 -0
- package/dist/tools/symbols.js +279 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +92 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sseClient.d.ts","sourceRoot":"","sources":["../../src/mcp/sseClient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AA6BjF,qBAAa,YAAa,YAAW,YAAY;IAWrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVjD,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0H;IACnJ,OAAO,CAAC,eAAe,CAAoE;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAmB,MAAM,EAAE,eAAe;IAIlE,IAAI,KAAK,IAAI,iBAAiB,EAAE,CAE/B;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAaxB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAc5E,UAAU,IAAI,IAAI;IAalB,4EAA4E;YAC9D,WAAW;IAoCzB,OAAO,CAAC,eAAe;IAYvB,oFAAoF;IACpF,OAAO,CAAC,KAAK;IAWb,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,cAAc;IAuCtB,OAAO,CAAC,IAAI;YAgBE,OAAO;IAIrB,6EAA6E;YAC/D,YAAY;CAwB3B"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.McpSseClient = void 0;
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
const httpClient_1 = require("./httpClient");
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
10
|
+
const ENDPOINT_TIMEOUT_MS = 15000;
|
|
11
|
+
class McpSseClient {
|
|
12
|
+
constructor(name, config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this._nextId = 1;
|
|
15
|
+
this._tools = [];
|
|
16
|
+
this._endpointUrl = null;
|
|
17
|
+
this._abort = null;
|
|
18
|
+
this._sseBuf = '';
|
|
19
|
+
this._closed = false;
|
|
20
|
+
this._pending = new Map();
|
|
21
|
+
this._endpointWaiter = null;
|
|
22
|
+
this.name = name;
|
|
23
|
+
}
|
|
24
|
+
get tools() {
|
|
25
|
+
return this._tools;
|
|
26
|
+
}
|
|
27
|
+
async connect() {
|
|
28
|
+
if (!this.config.url)
|
|
29
|
+
throw new Error(`MCP server "${this.name}" has no url (sse transport requires one).`);
|
|
30
|
+
await this._openStream();
|
|
31
|
+
await this._rpc('initialize', {
|
|
32
|
+
protocolVersion: '2024-11-05',
|
|
33
|
+
capabilities: {},
|
|
34
|
+
clientInfo: { name: 'nexrall-code', version: '1.0' },
|
|
35
|
+
});
|
|
36
|
+
await this._notify('notifications/initialized');
|
|
37
|
+
const res = (await this._rpc('tools/list', {}));
|
|
38
|
+
this._tools = res?.tools ?? [];
|
|
39
|
+
}
|
|
40
|
+
async callTool(name, args) {
|
|
41
|
+
const result = (await this._rpc('tools/call', { name, arguments: args }));
|
|
42
|
+
if (!result)
|
|
43
|
+
return '';
|
|
44
|
+
const text = (result.content ?? [])
|
|
45
|
+
.filter((c) => c.type === 'text')
|
|
46
|
+
.map((c) => c.text ?? '')
|
|
47
|
+
.join('\n');
|
|
48
|
+
if (result.isError)
|
|
49
|
+
throw new Error(text || 'MCP tool returned an error');
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
disconnect() {
|
|
53
|
+
this._closed = true;
|
|
54
|
+
this._abort?.abort();
|
|
55
|
+
this._abort = null;
|
|
56
|
+
for (const { reject, timer } of this._pending.values()) {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
reject(new Error('MCP SSE connection closed'));
|
|
59
|
+
}
|
|
60
|
+
this._pending.clear();
|
|
61
|
+
}
|
|
62
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
63
|
+
/** Open the GET event stream and wait for the server's `endpoint` event. */
|
|
64
|
+
async _openStream() {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
this._abort = controller;
|
|
67
|
+
const headers = {
|
|
68
|
+
Accept: 'text/event-stream',
|
|
69
|
+
...(this.config.headers ?? {}),
|
|
70
|
+
};
|
|
71
|
+
const resp = await (0, node_fetch_1.default)(this.config.url, { method: 'GET', headers, signal: controller.signal });
|
|
72
|
+
if (!resp.ok) {
|
|
73
|
+
if (resp.status === 401) {
|
|
74
|
+
throw new httpClient_1.McpOAuthRequiredError(this.name, this.config.url, resp.headers.get('www-authenticate') ?? '');
|
|
75
|
+
}
|
|
76
|
+
const body = await resp.text().catch(() => '');
|
|
77
|
+
throw new Error(`MCP SSE HTTP ${resp.status} ${resp.statusText}${body ? `: ${body.slice(0, 200)}` : ''}`);
|
|
78
|
+
}
|
|
79
|
+
if (!resp.body)
|
|
80
|
+
throw new Error('MCP SSE response has no body stream');
|
|
81
|
+
const stream = resp.body;
|
|
82
|
+
stream.on('data', (chunk) => this._feed(chunk.toString('utf-8')));
|
|
83
|
+
stream.on('end', () => this._onStreamClosed(new Error('MCP SSE stream ended')));
|
|
84
|
+
stream.on('error', (err) => { if (!this._closed)
|
|
85
|
+
this._onStreamClosed(err); });
|
|
86
|
+
// Wait for the `endpoint` event so we know where to POST messages.
|
|
87
|
+
await new Promise((resolve, reject) => {
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
this._endpointWaiter = null;
|
|
90
|
+
reject(new Error('Timed out waiting for SSE endpoint event'));
|
|
91
|
+
}, ENDPOINT_TIMEOUT_MS);
|
|
92
|
+
this._endpointWaiter = {
|
|
93
|
+
resolve: () => { clearTimeout(timer); resolve(); },
|
|
94
|
+
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
_onStreamClosed(err) {
|
|
99
|
+
if (this._closed)
|
|
100
|
+
return;
|
|
101
|
+
this._closed = true;
|
|
102
|
+
this._endpointWaiter?.reject(err);
|
|
103
|
+
this._endpointWaiter = null;
|
|
104
|
+
for (const { reject, timer } of this._pending.values()) {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
reject(err);
|
|
107
|
+
}
|
|
108
|
+
this._pending.clear();
|
|
109
|
+
}
|
|
110
|
+
/** Accumulate raw bytes and dispatch complete SSE events (blank-line delimited). */
|
|
111
|
+
_feed(text) {
|
|
112
|
+
this._sseBuf += text;
|
|
113
|
+
let sep;
|
|
114
|
+
// Events are separated by a blank line (\n\n or \r\n\r\n).
|
|
115
|
+
while ((sep = this._firstEventBoundary(this._sseBuf)) !== -1) {
|
|
116
|
+
const raw = this._sseBuf.slice(0, sep);
|
|
117
|
+
this._sseBuf = this._sseBuf.slice(sep).replace(/^(\r?\n)+/, '');
|
|
118
|
+
this._dispatchEvent(raw);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
_firstEventBoundary(buf) {
|
|
122
|
+
const a = buf.indexOf('\n\n');
|
|
123
|
+
const b = buf.indexOf('\r\n\r\n');
|
|
124
|
+
if (a === -1)
|
|
125
|
+
return b;
|
|
126
|
+
if (b === -1)
|
|
127
|
+
return a;
|
|
128
|
+
return Math.min(a, b);
|
|
129
|
+
}
|
|
130
|
+
_dispatchEvent(raw) {
|
|
131
|
+
let event = 'message';
|
|
132
|
+
const dataLines = [];
|
|
133
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
134
|
+
if (line.startsWith('event:'))
|
|
135
|
+
event = line.slice(6).trim();
|
|
136
|
+
else if (line.startsWith('data:'))
|
|
137
|
+
dataLines.push(line.slice(5).replace(/^ /, ''));
|
|
138
|
+
// ignore `id:`, `:` comments / pings, etc.
|
|
139
|
+
}
|
|
140
|
+
const data = dataLines.join('\n');
|
|
141
|
+
if (!data && event !== 'endpoint')
|
|
142
|
+
return;
|
|
143
|
+
if (event === 'endpoint') {
|
|
144
|
+
try {
|
|
145
|
+
this._endpointUrl = new URL(data, this.config.url).toString();
|
|
146
|
+
this._endpointWaiter?.resolve();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
this._endpointWaiter?.reject(new Error(`Invalid SSE endpoint URL: ${data}`));
|
|
150
|
+
}
|
|
151
|
+
this._endpointWaiter = null;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// event: message — a JSON-RPC response/notification.
|
|
155
|
+
let msg;
|
|
156
|
+
try {
|
|
157
|
+
msg = JSON.parse(data);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return; // non-JSON frame
|
|
161
|
+
}
|
|
162
|
+
if (typeof msg.id === 'number' && this._pending.has(msg.id)) {
|
|
163
|
+
const { resolve, reject, timer } = this._pending.get(msg.id);
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
this._pending.delete(msg.id);
|
|
166
|
+
if (msg.error)
|
|
167
|
+
reject(new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
|
|
168
|
+
else
|
|
169
|
+
resolve(msg.result ?? null);
|
|
170
|
+
}
|
|
171
|
+
// Server-initiated notifications (no matching id) are ignored.
|
|
172
|
+
}
|
|
173
|
+
_rpc(method, params) {
|
|
174
|
+
const id = this._nextId++;
|
|
175
|
+
const promise = new Promise((resolve, reject) => {
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
this._pending.delete(id);
|
|
178
|
+
reject(new Error(`MCP SSE request "${method}" timed out`));
|
|
179
|
+
}, REQUEST_TIMEOUT_MS);
|
|
180
|
+
this._pending.set(id, { resolve, reject, timer });
|
|
181
|
+
});
|
|
182
|
+
this._postMessage({ jsonrpc: '2.0', id, method, params }).catch((err) => {
|
|
183
|
+
const entry = this._pending.get(id);
|
|
184
|
+
if (entry) {
|
|
185
|
+
clearTimeout(entry.timer);
|
|
186
|
+
this._pending.delete(id);
|
|
187
|
+
entry.reject(err);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return promise;
|
|
191
|
+
}
|
|
192
|
+
async _notify(method, params) {
|
|
193
|
+
await this._postMessage({ jsonrpc: '2.0', method, params });
|
|
194
|
+
}
|
|
195
|
+
/** POST a JSON-RPC message to the endpoint discovered via the SSE stream. */
|
|
196
|
+
async _postMessage(body) {
|
|
197
|
+
if (!this._endpointUrl)
|
|
198
|
+
throw new Error('MCP SSE endpoint not established yet');
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
201
|
+
let resp;
|
|
202
|
+
try {
|
|
203
|
+
resp = await (0, node_fetch_1.default)(this._endpointUrl, {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: { 'Content-Type': 'application/json', ...(this.config.headers ?? {}) },
|
|
206
|
+
body: JSON.stringify(body),
|
|
207
|
+
signal: controller.signal,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
}
|
|
213
|
+
if (!resp.ok) {
|
|
214
|
+
if (resp.status === 401) {
|
|
215
|
+
throw new httpClient_1.McpOAuthRequiredError(this.name, this.config.url, resp.headers.get('www-authenticate') ?? '');
|
|
216
|
+
}
|
|
217
|
+
const text = await resp.text().catch(() => '');
|
|
218
|
+
throw new Error(`MCP SSE POST ${resp.status} ${resp.statusText}${text ? `: ${text.slice(0, 200)}` : ''}`);
|
|
219
|
+
}
|
|
220
|
+
// Body (if any) is ignored — the JSON-RPC response arrives over the SSE stream.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
exports.McpSseClient = McpSseClient;
|
|
224
|
+
//# sourceMappingURL=sseClient.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type PermissionDecision = 'allow' | 'ask' | 'deny';
|
|
2
|
+
export interface PermissionRules {
|
|
3
|
+
allow: string[];
|
|
4
|
+
ask: string[];
|
|
5
|
+
deny: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface MergedSettings {
|
|
8
|
+
permissions: PermissionRules;
|
|
9
|
+
/** Raw merged object (for hooks, allowedTools, etc.). */
|
|
10
|
+
raw: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** Merge the settings tiers (deny/allow/ask arrays are unioned). */
|
|
13
|
+
export declare function loadSettings(workDir: string): MergedSettings;
|
|
14
|
+
/**
|
|
15
|
+
* Evaluate a tool call against merged rules. Returns the matching decision, or
|
|
16
|
+
* null when no rule applies (caller decides the default, usually "ask").
|
|
17
|
+
* Precedence: deny → allow → ask.
|
|
18
|
+
*/
|
|
19
|
+
export declare function evaluatePermission(rules: PermissionRules, tool: string, input: Record<string, unknown>, workDir: string): PermissionDecision | null;
|
|
20
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAsBD,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAoB5D;AAqHD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,kBAAkB,GAAG,IAAI,CAK3B"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadSettings = loadSettings;
|
|
37
|
+
exports.evaluatePermission = evaluatePermission;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
function managedSettingsPath() {
|
|
42
|
+
if (process.platform === 'darwin')
|
|
43
|
+
return '/Library/Application Support/Nexrall/managed-settings.json';
|
|
44
|
+
if (process.platform === 'win32')
|
|
45
|
+
return path.join(process.env.PROGRAMDATA ?? 'C:\\ProgramData', 'Nexrall', 'managed-settings.json');
|
|
46
|
+
return '/etc/nexrall/managed-settings.json';
|
|
47
|
+
}
|
|
48
|
+
function readJson(file) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function extractRules(obj) {
|
|
57
|
+
const p = (obj?.permissions ?? {});
|
|
58
|
+
const arr = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : []);
|
|
59
|
+
return { allow: arr(p.allow), ask: arr(p.ask), deny: arr(p.deny) };
|
|
60
|
+
}
|
|
61
|
+
/** Merge the settings tiers (deny/allow/ask arrays are unioned). */
|
|
62
|
+
function loadSettings(workDir) {
|
|
63
|
+
const tiers = [
|
|
64
|
+
managedSettingsPath(),
|
|
65
|
+
path.join(os.homedir(), '.nexrall', 'settings.json'),
|
|
66
|
+
path.join(workDir, '.nexrall', 'settings.json'),
|
|
67
|
+
path.join(workDir, '.nexrall', 'settings.local.json'),
|
|
68
|
+
];
|
|
69
|
+
const merged = { allow: [], ask: [], deny: [] };
|
|
70
|
+
const raw = {};
|
|
71
|
+
for (const file of tiers) {
|
|
72
|
+
const obj = readJson(file);
|
|
73
|
+
if (!obj)
|
|
74
|
+
continue;
|
|
75
|
+
Object.assign(raw, obj);
|
|
76
|
+
const r = extractRules(obj);
|
|
77
|
+
merged.allow.push(...r.allow);
|
|
78
|
+
merged.ask.push(...r.ask);
|
|
79
|
+
merged.deny.push(...r.deny);
|
|
80
|
+
}
|
|
81
|
+
return { permissions: merged, raw };
|
|
82
|
+
}
|
|
83
|
+
// ─── Rule matching ──────────────────────────────────────────────────────────
|
|
84
|
+
function parseRule(rule) {
|
|
85
|
+
const m = /^([A-Za-z0-9_]+)\((.*)\)$/.exec(rule.trim());
|
|
86
|
+
if (m)
|
|
87
|
+
return { tool: m[1], pattern: m[2] };
|
|
88
|
+
return { tool: rule.trim(), pattern: null };
|
|
89
|
+
}
|
|
90
|
+
function globToRegExp(glob) {
|
|
91
|
+
// Process token by token so ** and * are handled without a placeholder.
|
|
92
|
+
// The old approach used a null byte / control-char sentinel which (a) made
|
|
93
|
+
// the file appear binary to grep/tools, and (b) broke paths containing spaces
|
|
94
|
+
// (the space placeholder was replaced back even in non-glob contexts).
|
|
95
|
+
let re = '';
|
|
96
|
+
for (let i = 0; i < glob.length; i++) {
|
|
97
|
+
const ch = glob[i];
|
|
98
|
+
if (ch === '*' && glob[i + 1] === '*') {
|
|
99
|
+
re += '.*'; // ** → match across path separators
|
|
100
|
+
i++; // skip second *
|
|
101
|
+
}
|
|
102
|
+
else if (ch === '*') {
|
|
103
|
+
re += '[^/]*'; // * → match within one segment
|
|
104
|
+
}
|
|
105
|
+
else if (ch === '?') {
|
|
106
|
+
re += '[^/]'; // ? → single non-separator char
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Escape regex metacharacters literally
|
|
110
|
+
re += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return new RegExp(`^${re}$`);
|
|
114
|
+
}
|
|
115
|
+
function matchPattern(pattern, value) {
|
|
116
|
+
if (!value)
|
|
117
|
+
return false;
|
|
118
|
+
// "name:*" → prefix match (Claude Bash semantics)
|
|
119
|
+
if (pattern.endsWith(':*')) {
|
|
120
|
+
const prefix = pattern.slice(0, -2);
|
|
121
|
+
return value === prefix || value.startsWith(prefix);
|
|
122
|
+
}
|
|
123
|
+
if (pattern.includes('*') || pattern.includes('?')) {
|
|
124
|
+
try {
|
|
125
|
+
return globToRegExp(pattern).test(value);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return value === pattern;
|
|
132
|
+
}
|
|
133
|
+
/** Map a tool's input to the string(s) a rule pattern should match against. */
|
|
134
|
+
function matchValues(tool, input, workDir) {
|
|
135
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
136
|
+
const vals = [];
|
|
137
|
+
const pushPath = (p) => {
|
|
138
|
+
if (!p)
|
|
139
|
+
return;
|
|
140
|
+
vals.push(p);
|
|
141
|
+
if (path.isAbsolute(p))
|
|
142
|
+
vals.push(path.relative(workDir, p));
|
|
143
|
+
else
|
|
144
|
+
vals.push(path.resolve(workDir, p));
|
|
145
|
+
};
|
|
146
|
+
switch (tool) {
|
|
147
|
+
case 'bash':
|
|
148
|
+
vals.push(str(input.command));
|
|
149
|
+
break;
|
|
150
|
+
case 'move_file':
|
|
151
|
+
pushPath(str(input.source));
|
|
152
|
+
pushPath(str(input.dest));
|
|
153
|
+
break;
|
|
154
|
+
case 'copy_file':
|
|
155
|
+
pushPath(str(input.source));
|
|
156
|
+
pushPath(str(input.destination));
|
|
157
|
+
break;
|
|
158
|
+
case 'fetch_url':
|
|
159
|
+
vals.push(str(input.url));
|
|
160
|
+
break;
|
|
161
|
+
case 'generate_image':
|
|
162
|
+
vals.push(str(input.path));
|
|
163
|
+
break;
|
|
164
|
+
case 'stock_photo':
|
|
165
|
+
vals.push(str(input.path));
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
pushPath(str(input.path));
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
return vals.filter(Boolean);
|
|
172
|
+
}
|
|
173
|
+
function ruleMatches(rule, tool, input, workDir, opts) {
|
|
174
|
+
const { tool: ruleTool, pattern } = parseRule(rule);
|
|
175
|
+
// Allow a few friendly aliases so rules read naturally (Claude-style names).
|
|
176
|
+
const aliases = {
|
|
177
|
+
Read: ['read_file', 'list_directory', 'search_files', 'glob', 'notebook_read'],
|
|
178
|
+
Edit: ['edit_file', 'multi_edit', 'write_file', 'notebook_edit'],
|
|
179
|
+
Write: ['write_file'],
|
|
180
|
+
Bash: ['bash'],
|
|
181
|
+
WebFetch: ['fetch_url'],
|
|
182
|
+
Image: ['generate_image', 'stock_photo'],
|
|
183
|
+
};
|
|
184
|
+
const matchesTool = ruleTool === tool ||
|
|
185
|
+
ruleTool === '*' ||
|
|
186
|
+
(aliases[ruleTool]?.includes(tool) ?? false);
|
|
187
|
+
if (!matchesTool)
|
|
188
|
+
return false;
|
|
189
|
+
// Multi-line guard for ALLOW rules on bash: a patterned allow rule
|
|
190
|
+
// (prefix or glob) must never auto-approve a multi-line command — a newline
|
|
191
|
+
// acts like `;`, so "npm run test\nrm -rf ~" would otherwise ride an
|
|
192
|
+
// "Bash(npm run test:*)" allow rule straight past the user prompt.
|
|
193
|
+
// Deny rules stay aggressive (they SHOULD match multi-line commands), and a
|
|
194
|
+
// bare "Bash" allow (pattern === null) is an explicit "allow everything".
|
|
195
|
+
if (opts?.forAllow && tool === 'bash' && pattern !== null && pattern !== '' && pattern !== '*') {
|
|
196
|
+
const cmd = typeof input.command === 'string' ? input.command : '';
|
|
197
|
+
if (/[\r\n]/.test(cmd))
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (pattern === null || pattern === '' || pattern === '*')
|
|
201
|
+
return true;
|
|
202
|
+
return matchValues(tool, input, workDir).some((v) => matchPattern(pattern, v));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Evaluate a tool call against merged rules. Returns the matching decision, or
|
|
206
|
+
* null when no rule applies (caller decides the default, usually "ask").
|
|
207
|
+
* Precedence: deny → allow → ask.
|
|
208
|
+
*/
|
|
209
|
+
function evaluatePermission(rules, tool, input, workDir) {
|
|
210
|
+
if (rules.deny.some((r) => ruleMatches(r, tool, input, workDir)))
|
|
211
|
+
return 'deny';
|
|
212
|
+
if (rules.allow.some((r) => ruleMatches(r, tool, input, workDir, { forAllow: true })))
|
|
213
|
+
return 'allow';
|
|
214
|
+
if (rules.ask.some((r) => ruleMatches(r, tool, input, workDir)))
|
|
215
|
+
return 'ask';
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PluginInfo {
|
|
2
|
+
/** Directory name (or plugin.json "name" when present). */
|
|
3
|
+
name: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
/** Absolute path to the plugin directory. */
|
|
7
|
+
dir: string;
|
|
8
|
+
scope: 'project' | 'global';
|
|
9
|
+
}
|
|
10
|
+
/** Discover installed plugins (project scope shadows global on name clash). */
|
|
11
|
+
export declare function loadPlugins(workDir: string): PluginInfo[];
|
|
12
|
+
/** Subdirectories of every installed plugin that hold `kind` assets (existing only). */
|
|
13
|
+
export declare function pluginAssetDirs(workDir: string, kind: 'commands' | 'agents'): string[];
|
|
14
|
+
/** Merge hooks.json from every plugin into a single hooks config (arrays concatenated). */
|
|
15
|
+
export declare function pluginHooks(workDir: string): Record<string, unknown[]>;
|
|
16
|
+
/** MCP server maps declared by plugins: { serverName → config } (first plugin wins on clash). */
|
|
17
|
+
export declare function pluginMcpServers(workDir: string): Record<string, unknown>;
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;CAC7B;AAsCD,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAKzD;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,MAAM,EAAE,CAMtF;AAED,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAetE;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazE"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadPlugins = loadPlugins;
|
|
37
|
+
exports.pluginAssetDirs = pluginAssetDirs;
|
|
38
|
+
exports.pluginHooks = pluginHooks;
|
|
39
|
+
exports.pluginMcpServers = pluginMcpServers;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
function readMeta(dir) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = fs.readFileSync(path.join(dir, 'plugin.json'), 'utf-8');
|
|
46
|
+
const j = JSON.parse(raw);
|
|
47
|
+
return {
|
|
48
|
+
name: typeof j.name === 'string' ? j.name : undefined,
|
|
49
|
+
version: typeof j.version === 'string' ? j.version : undefined,
|
|
50
|
+
description: typeof j.description === 'string' ? j.description : undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function scanRoot(root, scope, into) {
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
67
|
+
continue;
|
|
68
|
+
const dir = path.join(root, entry.name);
|
|
69
|
+
try {
|
|
70
|
+
if (!fs.statSync(dir).isDirectory())
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const meta = readMeta(dir);
|
|
77
|
+
const name = meta.name || entry.name;
|
|
78
|
+
if (into.has(name))
|
|
79
|
+
continue; // project scope scanned first — it wins
|
|
80
|
+
into.set(name, { name, version: meta.version, description: meta.description, dir, scope });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Discover installed plugins (project scope shadows global on name clash). */
|
|
84
|
+
function loadPlugins(workDir) {
|
|
85
|
+
const out = new Map();
|
|
86
|
+
scanRoot(path.join(workDir, '.nexrall', 'plugins'), 'project', out);
|
|
87
|
+
scanRoot(path.join(os.homedir(), '.nexrall', 'plugins'), 'global', out);
|
|
88
|
+
return [...out.values()];
|
|
89
|
+
}
|
|
90
|
+
/** Subdirectories of every installed plugin that hold `kind` assets (existing only). */
|
|
91
|
+
function pluginAssetDirs(workDir, kind) {
|
|
92
|
+
return loadPlugins(workDir)
|
|
93
|
+
.map((p) => path.join(p.dir, kind))
|
|
94
|
+
.filter((d) => {
|
|
95
|
+
try {
|
|
96
|
+
return fs.statSync(d).isDirectory();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Merge hooks.json from every plugin into a single hooks config (arrays concatenated). */
|
|
104
|
+
function pluginHooks(workDir) {
|
|
105
|
+
const merged = {};
|
|
106
|
+
for (const p of loadPlugins(workDir)) {
|
|
107
|
+
try {
|
|
108
|
+
const raw = fs.readFileSync(path.join(p.dir, 'hooks.json'), 'utf-8');
|
|
109
|
+
const j = JSON.parse(raw);
|
|
110
|
+
// Accept either { hooks: {...} } or the bare hooks object.
|
|
111
|
+
const hooks = (j.hooks ?? j);
|
|
112
|
+
for (const [phase, entries] of Object.entries(hooks)) {
|
|
113
|
+
if (!Array.isArray(entries))
|
|
114
|
+
continue;
|
|
115
|
+
merged[phase] = [...(merged[phase] ?? []), ...entries];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch { /* no hooks.json or malformed — skip */ }
|
|
119
|
+
}
|
|
120
|
+
return merged;
|
|
121
|
+
}
|
|
122
|
+
/** MCP server maps declared by plugins: { serverName → config } (first plugin wins on clash). */
|
|
123
|
+
function pluginMcpServers(workDir) {
|
|
124
|
+
const merged = {};
|
|
125
|
+
for (const p of loadPlugins(workDir)) {
|
|
126
|
+
try {
|
|
127
|
+
const raw = fs.readFileSync(path.join(p.dir, 'mcp.json'), 'utf-8');
|
|
128
|
+
const j = JSON.parse(raw);
|
|
129
|
+
const servers = (j.mcpServers ?? j);
|
|
130
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
131
|
+
if (!(name in merged))
|
|
132
|
+
merged[name] = cfg;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch { /* no mcp.json — skip */ }
|
|
136
|
+
}
|
|
137
|
+
return merged;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ToolResult } from '../types';
|
|
2
|
+
import { type SandboxConfig } from './sandbox';
|
|
3
|
+
export declare function executeTool(name: string, input: Record<string, unknown>, abortSignal?: {
|
|
4
|
+
aborted: boolean;
|
|
5
|
+
}, sandbox?: SandboxConfig, workDir?: string, agentScope?: string): Promise<ToolResult>;
|
|
6
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AA46CtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
|