@nexrall/code-core 1.4.66 → 1.4.68
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/agentRegistry.d.ts +27 -0
- package/dist/agent/agentRegistry.d.ts.map +1 -1
- package/dist/agent/agentRegistry.js +45 -0
- package/dist/agent/agentTypes.d.ts +6 -2
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +13 -4
- package/dist/agent/askOnce.d.ts +12 -0
- package/dist/agent/askOnce.d.ts.map +1 -0
- package/dist/agent/askOnce.js +41 -0
- package/dist/agent/compaction.d.ts +244 -0
- package/dist/agent/compaction.d.ts.map +1 -0
- package/dist/agent/compaction.js +976 -0
- package/dist/agent/fileLocks.d.ts +34 -0
- package/dist/agent/fileLocks.d.ts.map +1 -0
- package/dist/agent/fileLocks.js +114 -0
- package/dist/agent/hooks.d.ts +331 -0
- package/dist/agent/hooks.d.ts.map +1 -0
- package/dist/agent/hooks.js +1239 -0
- package/dist/agent/iterationPolicy.d.ts +121 -0
- package/dist/agent/iterationPolicy.d.ts.map +1 -0
- package/dist/agent/iterationPolicy.js +297 -0
- package/dist/agent/lifecycleHost.d.ts +55 -0
- package/dist/agent/lifecycleHost.d.ts.map +1 -0
- package/dist/agent/lifecycleHost.js +294 -0
- package/dist/agent/loop.d.ts +39 -491
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +538 -3069
- package/dist/agent/planMode.d.ts.map +1 -1
- package/dist/agent/planMode.js +1 -0
- package/dist/agent/sharedTasks.d.ts +7 -0
- package/dist/agent/sharedTasks.d.ts.map +1 -1
- package/dist/agent/sharedTasks.js +16 -0
- package/dist/agent/subAgentBudget.d.ts +65 -0
- package/dist/agent/subAgentBudget.d.ts.map +1 -0
- package/dist/agent/subAgentBudget.js +269 -0
- package/dist/agent/subTask.d.ts +15 -0
- package/dist/agent/subTask.d.ts.map +1 -0
- package/dist/agent/subTask.js +730 -0
- package/dist/agent/subTaskSupport.d.ts +167 -0
- package/dist/agent/subTaskSupport.d.ts.map +1 -0
- package/dist/agent/subTaskSupport.js +425 -0
- package/dist/agent/toolDescriptions.d.ts +3 -0
- package/dist/agent/toolDescriptions.d.ts.map +1 -0
- package/dist/agent/toolDescriptions.js +116 -0
- package/dist/api/client.d.ts +41 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +173 -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/client.d.ts +104 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +136 -2
- package/dist/mcp/httpClient.d.ts +17 -1
- package/dist/mcp/httpClient.d.ts.map +1 -1
- package/dist/mcp/httpClient.js +120 -19
- package/dist/mcp/manager.d.ts +77 -2
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +275 -9
- package/dist/mcp/server.d.ts +39 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +189 -0
- package/dist/mcp/sseClient.d.ts +7 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +45 -1
- package/dist/mcp/stats.d.ts +41 -0
- package/dist/mcp/stats.d.ts.map +1 -0
- package/dist/mcp/stats.js +108 -0
- package/dist/permissions/destructive.d.ts +2 -0
- package/dist/permissions/destructive.d.ts.map +1 -1
- package/dist/permissions/destructive.js +6 -2
- package/dist/permissions/destructiveTokens.d.ts +5 -0
- package/dist/permissions/destructiveTokens.d.ts.map +1 -1
- package/dist/permissions/destructiveTokens.js +9 -3
- package/dist/permissions/modePolicy.d.ts.map +1 -1
- package/dist/permissions/modePolicy.js +5 -2
- package/dist/permissions/rules.d.ts +4 -1
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +29 -0
- package/dist/plugins/data.d.ts +23 -0
- package/dist/plugins/data.d.ts.map +1 -0
- package/dist/plugins/data.js +141 -0
- package/dist/plugins/index.d.ts +14 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +45 -4
- package/dist/plugins/installer.d.ts +83 -0
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +207 -1
- package/dist/types.d.ts +45 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/sseClient.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.McpSseClient = void 0;
|
|
7
7
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
const client_1 = require("./client");
|
|
8
9
|
const httpClient_1 = require("./httpClient");
|
|
9
10
|
const REQUEST_TIMEOUT_MS = 30000;
|
|
10
11
|
const ENDPOINT_TIMEOUT_MS = 15000;
|
|
@@ -19,24 +20,53 @@ class McpSseClient {
|
|
|
19
20
|
this._closed = false;
|
|
20
21
|
this._pending = new Map();
|
|
21
22
|
this._endpointWaiter = null;
|
|
23
|
+
this._elicitationHandler = null;
|
|
22
24
|
this.name = name;
|
|
23
25
|
}
|
|
24
26
|
get tools() {
|
|
25
27
|
return this._tools;
|
|
26
28
|
}
|
|
29
|
+
setElicitationHandler(handler) { this._elicitationHandler = handler; }
|
|
27
30
|
async connect() {
|
|
28
31
|
if (!this.config.url)
|
|
29
32
|
throw new Error(`MCP server "${this.name}" has no url (sse transport requires one).`);
|
|
30
33
|
await this._openStream();
|
|
31
34
|
await this._rpc('initialize', {
|
|
32
35
|
protocolVersion: '2024-11-05',
|
|
33
|
-
capabilities:
|
|
36
|
+
capabilities: (0, client_1.elicitationCapability)(this.config.bareElicitationCapability),
|
|
34
37
|
clientInfo: { name: 'nexrall-code', version: '1.0' },
|
|
35
38
|
});
|
|
36
39
|
await this._notify('notifications/initialized');
|
|
37
40
|
const res = (await this._rpc('tools/list', {}));
|
|
38
41
|
this._tools = res?.tools ?? [];
|
|
39
42
|
}
|
|
43
|
+
async listResources() {
|
|
44
|
+
try {
|
|
45
|
+
const res = (await this._rpc('resources/list', {}));
|
|
46
|
+
return Array.isArray(res?.resources) ? res.resources : [];
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return []; /* server has no resources capability */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async readResource(uri) {
|
|
53
|
+
return (0, client_1.flattenResourceContents)(await this._rpc('resources/read', { uri }));
|
|
54
|
+
}
|
|
55
|
+
async listPrompts() {
|
|
56
|
+
try {
|
|
57
|
+
const res = (await this._rpc('prompts/list', {}));
|
|
58
|
+
return Array.isArray(res?.prompts) ? res.prompts : [];
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return []; /* server has no prompts capability */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async getPrompt(name, args) {
|
|
65
|
+
const params = { name };
|
|
66
|
+
if (Object.keys(args).length)
|
|
67
|
+
params.arguments = args;
|
|
68
|
+
return (0, client_1.flattenPromptMessages)(await this._rpc('prompts/get', params));
|
|
69
|
+
}
|
|
40
70
|
async callTool(name, args) {
|
|
41
71
|
const result = (await this._rpc('tools/call', { name, arguments: args }));
|
|
42
72
|
if (!result)
|
|
@@ -159,6 +189,19 @@ class McpSseClient {
|
|
|
159
189
|
catch {
|
|
160
190
|
return; // non-JSON frame
|
|
161
191
|
}
|
|
192
|
+
// A server→client REQUEST (elicitation) — answer it over the POST channel.
|
|
193
|
+
// Checked first: server request ids are their own namespace and could collide
|
|
194
|
+
// with one of ours.
|
|
195
|
+
if (typeof msg.method === 'string' && msg.id != null) {
|
|
196
|
+
const method = msg.method;
|
|
197
|
+
const params = msg.params;
|
|
198
|
+
const reqId = msg.id;
|
|
199
|
+
void (async () => {
|
|
200
|
+
const reply = await (0, client_1.serverRequestReply)(this.name, this._elicitationHandler, method, params);
|
|
201
|
+
await this._postMessage({ jsonrpc: '2.0', id: reqId, ...reply });
|
|
202
|
+
})().catch(() => { });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
162
205
|
if (typeof msg.id === 'number' && this._pending.has(msg.id)) {
|
|
163
206
|
const { resolve, reject, timer } = this._pending.get(msg.id);
|
|
164
207
|
clearTimeout(timer);
|
|
@@ -167,6 +210,7 @@ class McpSseClient {
|
|
|
167
210
|
reject(new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
|
|
168
211
|
else
|
|
169
212
|
resolve(msg.result ?? null);
|
|
213
|
+
return;
|
|
170
214
|
}
|
|
171
215
|
// Server-initiated notifications (no matching id) are ignored.
|
|
172
216
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ToolStat {
|
|
2
|
+
calls: number;
|
|
3
|
+
failures: number;
|
|
4
|
+
/** Total wall time across every call, for an average that ignores the outlier size. */
|
|
5
|
+
totalMs: number;
|
|
6
|
+
/** The most recent call's duration — what a user means by "how long does it take". */
|
|
7
|
+
lastMs: number;
|
|
8
|
+
lastError?: string;
|
|
9
|
+
/** Epoch ms of the last call, so a stale number can be recognised as stale. */
|
|
10
|
+
lastAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ServerTotals {
|
|
13
|
+
calls: number;
|
|
14
|
+
failures: number;
|
|
15
|
+
/** Calls that neither succeeded nor failed — still in flight or abandoned. */
|
|
16
|
+
pending: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class ToolStats {
|
|
19
|
+
private readonly _byTool;
|
|
20
|
+
private _pending;
|
|
21
|
+
/** A call has started. Pair with exactly one ok()/fail(). */
|
|
22
|
+
begin(): void;
|
|
23
|
+
ok(name: string, ms: number, now?: number): void;
|
|
24
|
+
fail(name: string, ms: number, error: string, now?: number): void;
|
|
25
|
+
/** Everything recorded for one tool, or undefined if it was never called. */
|
|
26
|
+
get(name: string): ToolStat | undefined;
|
|
27
|
+
/** Stats for every tool whose name starts with `<server>__`, keyed by the bare tool name. */
|
|
28
|
+
forServer(server: string, prefix?: string): Array<{
|
|
29
|
+
tool: string;
|
|
30
|
+
stat: ToolStat;
|
|
31
|
+
}>;
|
|
32
|
+
forServerTotals(server: string, prefix?: string): ServerTotals;
|
|
33
|
+
/** Forgets one tool's history — so a fixed server stops showing old failures. */
|
|
34
|
+
reset(name?: string): void;
|
|
35
|
+
private _entry;
|
|
36
|
+
}
|
|
37
|
+
/** "240ms" / "1.2s" — a latency reading, not a stopwatch. */
|
|
38
|
+
export declare function formatMs(ms: number): string;
|
|
39
|
+
/** One line per tool, or '' when the server was never called (nothing to say yet). */
|
|
40
|
+
export declare function describeStats(stat: ToolStat | undefined, avg?: boolean): string;
|
|
41
|
+
//# sourceMappingURL=stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/mcp/stats.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,QAAQ,CAAK;IAErB,6DAA6D;IAC7D,KAAK,IAAI,IAAI;IAIb,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,IAAI;IAUpD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,IAAI;IAWrE,6EAA6E;IAC7E,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAIvC,6FAA6F;IAC7F,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,SAAO,GAAG,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;IAYjF,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,SAAO,GAAG,YAAY;IAS5D,iFAAiF;IACjF,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAK1B,OAAO,CAAC,MAAM;CAKf;AAED,6DAA6D;AAC7D,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAI3C;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,EAAE,GAAG,UAAO,GAAG,MAAM,CAO5E"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Per-tool call statistics for MCP servers (spec §42 "Tool latency / failures").
|
|
3
|
+
//
|
|
4
|
+
// WHY: when an MCP tool call returns nothing useful, the user's only question is
|
|
5
|
+
// "is this server slow, or broken?" — and the answer existed nowhere. The manager
|
|
6
|
+
// saw every call (it is the only path a tool call can take) but kept no record, so
|
|
7
|
+
// a flaky server looked exactly like a healthy one until it failed outright.
|
|
8
|
+
//
|
|
9
|
+
// Kept in its own file, and kept PURE (the caller measures the elapsed time), so the
|
|
10
|
+
// bookkeeping is unit-testable without spawning an MCP server. Nothing here is a
|
|
11
|
+
// promise about the future: the numbers are "since this session started", which is
|
|
12
|
+
// exactly the window a user is reasoning about.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.ToolStats = void 0;
|
|
15
|
+
exports.formatMs = formatMs;
|
|
16
|
+
exports.describeStats = describeStats;
|
|
17
|
+
class ToolStats {
|
|
18
|
+
constructor() {
|
|
19
|
+
this._byTool = new Map();
|
|
20
|
+
this._pending = 0;
|
|
21
|
+
}
|
|
22
|
+
/** A call has started. Pair with exactly one ok()/fail(). */
|
|
23
|
+
begin() {
|
|
24
|
+
this._pending++;
|
|
25
|
+
}
|
|
26
|
+
ok(name, ms, now = Date.now()) {
|
|
27
|
+
this._pending = Math.max(0, this._pending - 1);
|
|
28
|
+
const s = this._entry(name);
|
|
29
|
+
s.calls++;
|
|
30
|
+
s.totalMs += Math.max(0, ms);
|
|
31
|
+
s.lastMs = Math.max(0, ms);
|
|
32
|
+
s.lastAt = now;
|
|
33
|
+
delete s.lastError;
|
|
34
|
+
}
|
|
35
|
+
fail(name, ms, error, now = Date.now()) {
|
|
36
|
+
this._pending = Math.max(0, this._pending - 1);
|
|
37
|
+
const s = this._entry(name);
|
|
38
|
+
s.calls++;
|
|
39
|
+
s.failures++;
|
|
40
|
+
s.totalMs += Math.max(0, ms);
|
|
41
|
+
s.lastMs = Math.max(0, ms);
|
|
42
|
+
s.lastError = error;
|
|
43
|
+
s.lastAt = now;
|
|
44
|
+
}
|
|
45
|
+
/** Everything recorded for one tool, or undefined if it was never called. */
|
|
46
|
+
get(name) {
|
|
47
|
+
return this._byTool.get(name);
|
|
48
|
+
}
|
|
49
|
+
/** Stats for every tool whose name starts with `<server>__`, keyed by the bare tool name. */
|
|
50
|
+
forServer(server, prefix = '__') {
|
|
51
|
+
const head = server + prefix;
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const [name, stat] of this._byTool) {
|
|
54
|
+
if (!name.startsWith(head))
|
|
55
|
+
continue;
|
|
56
|
+
out.push({ tool: name.slice(head.length), stat });
|
|
57
|
+
}
|
|
58
|
+
// Slowest / most used first: that is the row a user is looking for.
|
|
59
|
+
out.sort((a, b) => b.stat.calls - a.stat.calls || a.tool.localeCompare(b.tool));
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
forServerTotals(server, prefix = '__') {
|
|
63
|
+
const rows = this.forServer(server, prefix);
|
|
64
|
+
return {
|
|
65
|
+
calls: rows.reduce((n, r) => n + r.stat.calls, 0),
|
|
66
|
+
failures: rows.reduce((n, r) => n + r.stat.failures, 0),
|
|
67
|
+
pending: this._pending,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Forgets one tool's history — so a fixed server stops showing old failures. */
|
|
71
|
+
reset(name) {
|
|
72
|
+
if (name === undefined) {
|
|
73
|
+
this._byTool.clear();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this._byTool.delete(name);
|
|
77
|
+
}
|
|
78
|
+
_entry(name) {
|
|
79
|
+
let s = this._byTool.get(name);
|
|
80
|
+
if (!s) {
|
|
81
|
+
s = { calls: 0, failures: 0, totalMs: 0, lastMs: 0, lastAt: 0 };
|
|
82
|
+
this._byTool.set(name, s);
|
|
83
|
+
}
|
|
84
|
+
return s;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.ToolStats = ToolStats;
|
|
88
|
+
/** "240ms" / "1.2s" — a latency reading, not a stopwatch. */
|
|
89
|
+
function formatMs(ms) {
|
|
90
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
91
|
+
return '—';
|
|
92
|
+
if (ms < 1000)
|
|
93
|
+
return `${Math.round(ms)}ms`;
|
|
94
|
+
return `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)}s`;
|
|
95
|
+
}
|
|
96
|
+
/** One line per tool, or '' when the server was never called (nothing to say yet). */
|
|
97
|
+
function describeStats(stat, avg = true) {
|
|
98
|
+
if (!stat || stat.calls === 0)
|
|
99
|
+
return '';
|
|
100
|
+
const parts = [`${stat.calls} call${stat.calls === 1 ? '' : 's'}`];
|
|
101
|
+
if (stat.failures)
|
|
102
|
+
parts.push(`${stat.failures} failed`);
|
|
103
|
+
parts.push(`${avg && stat.calls > 1 ? 'avg' : 'last'} ${formatMs(avg && stat.calls > 1 ? stat.totalMs / stat.calls : stat.lastMs)}`);
|
|
104
|
+
if (stat.lastError)
|
|
105
|
+
parts.push(stat.lastError.split('\n')[0].slice(0, 80));
|
|
106
|
+
return parts.join(' · ');
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=stats.js.map
|
|
@@ -24,6 +24,8 @@ export interface DestructiveMatch {
|
|
|
24
24
|
category: string;
|
|
25
25
|
/** Short human-readable reason shown in the confirmation prompt. */
|
|
26
26
|
reason: string;
|
|
27
|
+
/** Contained harm — `auto` mode may skip the confirmation (see TokenMatch.lowRisk). */
|
|
28
|
+
lowRisk?: boolean;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* Classify a bash command. Returns the FIRST matching destructive category, or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"destructive.d.ts","sourceRoot":"","sources":["../../src/permissions/destructive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"destructive.d.ts","sourceRoot":"","sources":["../../src/permissions/destructive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAiQD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAGnF;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,GAAG,IAAI,CAQvG"}
|
|
@@ -251,12 +251,16 @@ function classify(command, depth) {
|
|
|
251
251
|
if (viaRegex)
|
|
252
252
|
return viaRegex;
|
|
253
253
|
// Token pass: understands option order, bundled flags, wrappers and `bash -c "…"`.
|
|
254
|
+
// A low-risk hit (scratch-file truncation) must not mask a real one later in the chain.
|
|
255
|
+
let soft = null;
|
|
254
256
|
for (const tokens of (0, destructiveTokens_1.tokenize)(stripHeredocs(command))) {
|
|
255
257
|
const m = (0, destructiveTokens_1.classifyTokens)(tokens, (inner) => classify(inner, depth + 1), classifyRawSql);
|
|
256
|
-
if (m)
|
|
258
|
+
if (m && !m.lowRisk)
|
|
257
259
|
return m;
|
|
260
|
+
if (m && !soft)
|
|
261
|
+
soft = m;
|
|
258
262
|
}
|
|
259
|
-
return
|
|
263
|
+
return soft;
|
|
260
264
|
}
|
|
261
265
|
/**
|
|
262
266
|
* Classify a bash command. Returns the FIRST matching destructive category, or
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
export interface TokenMatch {
|
|
23
23
|
category: string;
|
|
24
24
|
reason: string;
|
|
25
|
+
/**
|
|
26
|
+
* True when the harm is contained (e.g. truncating a scratch file under /tmp or a
|
|
27
|
+
* relative path). `auto` mode may run these without asking; every other mode still does.
|
|
28
|
+
*/
|
|
29
|
+
lowRisk?: boolean;
|
|
25
30
|
}
|
|
26
31
|
/** Split `src` into simple commands, each a list of unquoted words. */
|
|
27
32
|
export declare function tokenize(src: string): string[][];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"destructiveTokens.d.ts","sourceRoot":"","sources":["../../src/permissions/destructiveTokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"destructiveTokens.d.ts","sourceRoot":"","sources":["../../src/permissions/destructiveTokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CA2BhD;AAgQD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,EAC/C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,GAC7C,UAAU,GAAG,IAAI,CA8DnB"}
|
|
@@ -388,9 +388,15 @@ function classifyTokens(tokens, recurse, rawSql) {
|
|
|
388
388
|
const r = /^>(?![>&])/.test(t[0]) ? t : ((t[0] === ':' || t[0] === 'true') && /^>(?![>&])/.test(t[1] ?? '')) ? t.slice(1) : null;
|
|
389
389
|
if (r) {
|
|
390
390
|
const target = r[0].length > 1 ? r[0].slice(1) : (r[1] ?? '');
|
|
391
|
-
if (
|
|
392
|
-
return
|
|
393
|
-
|
|
391
|
+
if (target.startsWith('/dev/'))
|
|
392
|
+
return null;
|
|
393
|
+
const m = hit('filesystem', 'a bare `>` redirect truncates the file (contents lost)');
|
|
394
|
+
// Scratch/relative targets only: no `$var`, `~`, `..`; absolute paths must be under a temp dir.
|
|
395
|
+
const scratch = target !== '' && !/[$`~]|(^|\/)\.\.(\/|$)/.test(target)
|
|
396
|
+
&& (!target.startsWith('/') || /^\/(?:private\/)?(?:tmp|var\/folders)\//.test(target));
|
|
397
|
+
if (scratch)
|
|
398
|
+
m.lowRisk = true;
|
|
399
|
+
return m;
|
|
394
400
|
}
|
|
395
401
|
}
|
|
396
402
|
const bin = base(t[0]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modePolicy.d.ts","sourceRoot":"","sources":["../../src/permissions/modePolicy.ts"],"names":[],"mappings":"AAsBA,yDAAyD;AACzD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9D,kDAAkD;AAClD,MAAM,MAAM,cAAc,GACtB,OAAO,GACP,KAAK,GACL,MAAM,GACN,SAAS,CAAC;AAEd,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,EAAE,cAAc,CAAC;IACrB,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,eAAe,
|
|
1
|
+
{"version":3,"file":"modePolicy.d.ts","sourceRoot":"","sources":["../../src/permissions/modePolicy.ts"],"names":[],"mappings":"AAsBA,yDAAyD;AACzD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9D,kDAAkD;AAClD,MAAM,MAAM,cAAc,GACtB,OAAO,GACP,KAAK,GACL,MAAM,GACN,SAAS,CAAC;AAEd,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,EAAE,cAAc,CAAC;IACrB,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,eAAe,aAY1B,CAAC;AAEH,0CAA0C;AAC1C,eAAO,MAAM,gBAAgB,aAG3B,CAAC;AAEH,oEAAoE;AACpE,eAAO,MAAM,iBAAiB,aAE5B,CAAC;AAEH,sCAAsC;AACtC,eAAO,MAAM,UAAU,aAAsD,CAAC;AAE9E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,cAAc,CA0DvD;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAOzD;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAGtD"}
|
|
@@ -29,6 +29,7 @@ exports.READ_ONLY_TOOLS = new Set([
|
|
|
29
29
|
'fetch_url', 'web_search',
|
|
30
30
|
'bash_output', 'kill_shell',
|
|
31
31
|
'todo_read', 'todo_write',
|
|
32
|
+
'cron_list', 'mcp_list_resources', 'mcp_read_resource',
|
|
32
33
|
'memory_read',
|
|
33
34
|
'use_skill',
|
|
34
35
|
'notebook_read',
|
|
@@ -87,7 +88,9 @@ function decide(req) {
|
|
|
87
88
|
// Before bypass, before mode. `rm -rf`, `git push --force`, `DROP TABLE` are not
|
|
88
89
|
// things any mode should silently perform, and the strong confirm treatment (type
|
|
89
90
|
// the word "yes", no "approve all") is what makes it a real speed bump.
|
|
90
|
-
|
|
91
|
+
// EXCEPT `auto`: the user chose "run everything on your own", so it does not stop here
|
|
92
|
+
// (hard-blocked commands such as `rm -rf /` are still denied by the executor).
|
|
93
|
+
if (req.destructive && mode !== 'auto')
|
|
91
94
|
return 'confirm';
|
|
92
95
|
// 3. Plan mode: refuse every remaining (i.e. mutating) tool outright.
|
|
93
96
|
// A refusal, not a prompt — the guarantee is "nothing changes until I approve the
|
|
@@ -135,7 +138,7 @@ function describeMode(mode) {
|
|
|
135
138
|
case 'plan': return 'plan — read-only, produces a plan for approval';
|
|
136
139
|
case 'ask': return 'ask — approves reads; asks before edits and commands';
|
|
137
140
|
case 'edit': return 'edit — edits files freely; asks before shell commands';
|
|
138
|
-
case 'auto': return 'auto — runs every tool on its own
|
|
141
|
+
case 'auto': return 'auto — runs every tool on its own, including irreversible commands';
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
/** Normalise arbitrary input to a known mode, defaulting to the safe middle. */
|
|
@@ -9,7 +9,10 @@ export interface MergedSettings {
|
|
|
9
9
|
/** Raw merged object (for hooks, allowedTools, etc.). */
|
|
10
10
|
raw: Record<string, unknown>;
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
12
|
+
/** Replace the rules supplied for this process by flags. Not persisted anywhere. */
|
|
13
|
+
export declare function setSessionRules(r: Partial<PermissionRules>): void;
|
|
14
|
+
export declare function setSessionSettings(obj: Record<string, unknown> | null): void;
|
|
15
|
+
export declare function getSessionSettings(): Record<string, unknown> | null;
|
|
13
16
|
export declare function loadSettings(workDir: string): MergedSettings;
|
|
14
17
|
/**
|
|
15
18
|
* Evaluate a tool call against merged rules. Returns the matching decision, or
|
|
@@ -1 +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;
|
|
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;AA4BD,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAEjE;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,IAAI,CAA2B;AACxG,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAA4B;AAEhG,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CA8B5D;AA8ID;;;;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"}
|
|
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.setSessionRules = setSessionRules;
|
|
37
|
+
exports.setSessionSettings = setSessionSettings;
|
|
38
|
+
exports.getSessionSettings = getSessionSettings;
|
|
36
39
|
exports.loadSettings = loadSettings;
|
|
37
40
|
exports.evaluatePermission = evaluatePermission;
|
|
38
41
|
const fs = __importStar(require("fs"));
|
|
@@ -58,7 +61,23 @@ function extractRules(obj) {
|
|
|
58
61
|
const arr = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : []);
|
|
59
62
|
return { allow: arr(p.allow), ask: arr(p.ask), deny: arr(p.deny) };
|
|
60
63
|
}
|
|
64
|
+
// Session-scoped rules from CLI flags (--allowedTools / --disallowedTools). Held here, not
|
|
65
|
+
// in the CLI's permission handler, because core's own gates (e.g. the `task` deny check in
|
|
66
|
+
// the agent loop) call loadSettings() directly — a flag that only the handler saw would let
|
|
67
|
+
// a sub-agent dispatch walk straight past it.
|
|
68
|
+
let sessionRules = { allow: [], ask: [], deny: [] };
|
|
69
|
+
/** Replace the rules supplied for this process by flags. Not persisted anywhere. */
|
|
70
|
+
function setSessionRules(r) {
|
|
71
|
+
sessionRules = { allow: [...(r.allow ?? [])], ask: [...(r.ask ?? [])], deny: [...(r.deny ?? [])] };
|
|
72
|
+
}
|
|
61
73
|
/** Merge the settings tiers (deny/allow/ask arrays are unioned). */
|
|
74
|
+
// A settings object supplied for this run only (`--settings <file|json>`). It is the LAST file
|
|
75
|
+
// tier: it overrides user/project values key-by-key (like the files do among themselves) and
|
|
76
|
+
// its permission rules ADD to theirs. Held here, with the rules above, because core's own gates
|
|
77
|
+
// call loadSettings() directly.
|
|
78
|
+
let sessionSettings = null;
|
|
79
|
+
function setSessionSettings(obj) { sessionSettings = obj; }
|
|
80
|
+
function getSessionSettings() { return sessionSettings; }
|
|
62
81
|
function loadSettings(workDir) {
|
|
63
82
|
const tiers = [
|
|
64
83
|
managedSettingsPath(),
|
|
@@ -78,6 +97,16 @@ function loadSettings(workDir) {
|
|
|
78
97
|
merged.ask.push(...r.ask);
|
|
79
98
|
merged.deny.push(...r.deny);
|
|
80
99
|
}
|
|
100
|
+
if (sessionSettings) {
|
|
101
|
+
Object.assign(raw, sessionSettings);
|
|
102
|
+
const r = extractRules(sessionSettings);
|
|
103
|
+
merged.allow.push(...r.allow);
|
|
104
|
+
merged.ask.push(...r.ask);
|
|
105
|
+
merged.deny.push(...r.deny);
|
|
106
|
+
}
|
|
107
|
+
merged.allow.push(...sessionRules.allow);
|
|
108
|
+
merged.ask.push(...sessionRules.ask);
|
|
109
|
+
merged.deny.push(...sessionRules.deny);
|
|
81
110
|
return { permissions: merged, raw };
|
|
82
111
|
}
|
|
83
112
|
// ─── Rule matching ──────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Claude Code's rule: letters, digits, `_`, `-` survive; everything else becomes `-`. */
|
|
2
|
+
export declare function sanitizePluginId(name: string): string;
|
|
3
|
+
/** Where this plugin's persistent data lives. Pure — does not touch the disk. */
|
|
4
|
+
export declare function pluginDataDir(name: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* `pluginDataDir`, created if missing. Returns the path either way.
|
|
7
|
+
*
|
|
8
|
+
* Called right before a plugin's process is spawned with NEXRALL_PLUGIN_DATA in
|
|
9
|
+
* its environment, so the promise "the variable points at a directory you may
|
|
10
|
+
* write to" holds the moment the plugin can read it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensurePluginDataDir(name: string): string;
|
|
13
|
+
/** True when the plugin has data on disk from earlier runs. */
|
|
14
|
+
export declare function pluginHasData(name: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Raw bytes under a plugin's data directory (0 when missing). Bounded walk: a
|
|
17
|
+
* plugin can park a large tree here, and reporting the size must not be the
|
|
18
|
+
* thing that makes `nex plugin prune` slow, so entries are counted up to a cap.
|
|
19
|
+
*/
|
|
20
|
+
export declare function pluginDataSize(name: string, maxEntries?: number): number;
|
|
21
|
+
/** Remove a plugin's data directory. Returns true when something was removed. */
|
|
22
|
+
export declare function removePluginData(name: string): boolean;
|
|
23
|
+
//# sourceMappingURL=data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/plugins/data.ts"],"names":[],"mappings":"AAwBA,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMxD;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAMnD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,SAAO,GAAG,MAAM,CAiBtE;AAED,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAStD"}
|
|
@@ -0,0 +1,141 @@
|
|
|
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.sanitizePluginId = sanitizePluginId;
|
|
37
|
+
exports.pluginDataDir = pluginDataDir;
|
|
38
|
+
exports.ensurePluginDataDir = ensurePluginDataDir;
|
|
39
|
+
exports.pluginHasData = pluginHasData;
|
|
40
|
+
exports.pluginDataSize = pluginDataSize;
|
|
41
|
+
exports.removePluginData = removePluginData;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
// ─── Plugin data directories ──────────────────────────────────────────────────
|
|
46
|
+
//
|
|
47
|
+
// A plugin's own files are replaced on every update, so state a plugin wants to
|
|
48
|
+
// keep — caches, node_modules, a downloaded model, a database — cannot live
|
|
49
|
+
// inside its install directory. It gets a per-plugin data directory instead:
|
|
50
|
+
//
|
|
51
|
+
// ~/.nexrall/plugin-data/<id>/
|
|
52
|
+
//
|
|
53
|
+
// which survives updates and is exposed to the plugin's own processes (its hook
|
|
54
|
+
// commands and its MCP servers) as NEXRALL_PLUGIN_DATA. Same idea as Claude
|
|
55
|
+
// Code's ${CLAUDE_PLUGIN_DATA}, same id rule: everything outside [A-Za-z0-9_-]
|
|
56
|
+
// becomes "-", so a name can never escape the parent directory or collide with
|
|
57
|
+
// path syntax ("my.plugin" → "my-plugin"). Two names that differ only in those
|
|
58
|
+
// characters share a directory — which is why the id is derived, never taken
|
|
59
|
+
// verbatim.
|
|
60
|
+
//
|
|
61
|
+
// The directory is created on FIRST REFERENCE, not at install: a plugin that
|
|
62
|
+
// never touches its data dir leaves nothing behind, and `nex plugin remove`
|
|
63
|
+
// does not have to guess whether a directory it finds is in use.
|
|
64
|
+
/** Claude Code's rule: letters, digits, `_`, `-` survive; everything else becomes `-`. */
|
|
65
|
+
function sanitizePluginId(name) {
|
|
66
|
+
return name.replace(/[^\w-]/g, '-');
|
|
67
|
+
}
|
|
68
|
+
/** Where this plugin's persistent data lives. Pure — does not touch the disk. */
|
|
69
|
+
function pluginDataDir(name) {
|
|
70
|
+
return path.join(os.homedir(), '.nexrall', 'plugin-data', sanitizePluginId(name));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `pluginDataDir`, created if missing. Returns the path either way.
|
|
74
|
+
*
|
|
75
|
+
* Called right before a plugin's process is spawned with NEXRALL_PLUGIN_DATA in
|
|
76
|
+
* its environment, so the promise "the variable points at a directory you may
|
|
77
|
+
* write to" holds the moment the plugin can read it.
|
|
78
|
+
*/
|
|
79
|
+
function ensurePluginDataDir(name) {
|
|
80
|
+
const dir = pluginDataDir(name);
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
catch { /* unwritable home: the plugin will see the path and fail its own write */ }
|
|
85
|
+
return dir;
|
|
86
|
+
}
|
|
87
|
+
/** True when the plugin has data on disk from earlier runs. */
|
|
88
|
+
function pluginHasData(name) {
|
|
89
|
+
try {
|
|
90
|
+
return fs.readdirSync(pluginDataDir(name)).length > 0;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Raw bytes under a plugin's data directory (0 when missing). Bounded walk: a
|
|
98
|
+
* plugin can park a large tree here, and reporting the size must not be the
|
|
99
|
+
* thing that makes `nex plugin prune` slow, so entries are counted up to a cap.
|
|
100
|
+
*/
|
|
101
|
+
function pluginDataSize(name, maxEntries = 5000) {
|
|
102
|
+
let total = 0;
|
|
103
|
+
let seen = 0;
|
|
104
|
+
const walk = (dir) => {
|
|
105
|
+
let entries;
|
|
106
|
+
try {
|
|
107
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
for (const e of entries) {
|
|
113
|
+
if (seen++ >= maxEntries)
|
|
114
|
+
return;
|
|
115
|
+
const full = path.join(dir, e.name);
|
|
116
|
+
try {
|
|
117
|
+
if (e.isDirectory())
|
|
118
|
+
walk(full);
|
|
119
|
+
else
|
|
120
|
+
total += fs.statSync(full).size;
|
|
121
|
+
}
|
|
122
|
+
catch { /* unreadable entry — skip it, size is an estimate */ }
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
walk(pluginDataDir(name));
|
|
126
|
+
return total;
|
|
127
|
+
}
|
|
128
|
+
/** Remove a plugin's data directory. Returns true when something was removed. */
|
|
129
|
+
function removePluginData(name) {
|
|
130
|
+
const dir = pluginDataDir(name);
|
|
131
|
+
try {
|
|
132
|
+
if (!fs.existsSync(dir))
|
|
133
|
+
return false;
|
|
134
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=data.js.map
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -58,8 +58,20 @@ export declare function loadPlugins(workDir: string): PluginInfo[];
|
|
|
58
58
|
export declare function findPlugin(workDir: string, name: string, scope: 'project' | 'global'): PluginInfo | undefined;
|
|
59
59
|
/** Subdirectories of every ENABLED installed plugin that hold `kind` assets (existing only). */
|
|
60
60
|
export declare function pluginAssetDirs(workDir: string, kind: 'commands' | 'agents' | 'skills'): string[];
|
|
61
|
-
/** Merge hooks.json from every ENABLED plugin into a single hooks config (arrays concatenated).
|
|
61
|
+
/** Merge hooks.json from every ENABLED plugin into a single hooks config (arrays concatenated).
|
|
62
|
+
*
|
|
63
|
+
* Every handler is stamped with `plugin: <name>`. Provenance is the one thing the merge
|
|
64
|
+
* would otherwise destroy — and it is what lets the hook runner hand the plugin's own
|
|
65
|
+
* processes their NEXRALL_PLUGIN_DATA (see runHookHandler). Both entry forms are stamped:
|
|
66
|
+
* the flat one (the entry IS the handler) and the nested one (each item under `hooks`).
|
|
67
|
+
*/
|
|
62
68
|
export declare function pluginHooks(workDir: string): Record<string, unknown[]>;
|
|
63
|
-
/** MCP server maps declared by ENABLED plugins: { serverName → config } (first plugin wins on clash).
|
|
69
|
+
/** MCP server maps declared by ENABLED plugins: { serverName → config } (first plugin wins on clash).
|
|
70
|
+
*
|
|
71
|
+
* A stdio server from a plugin gets NEXRALL_PLUGIN_DATA in its environment, pointing at the
|
|
72
|
+
* plugin's persistent data directory — the install directory is replaced on every update, so
|
|
73
|
+
* this is the only place a server can keep state. A plugin that set the variable itself keeps
|
|
74
|
+
* its own value: an explicit config value is never overridden here.
|
|
75
|
+
*/
|
|
64
76
|
export declare function pluginMcpServers(workDir: string): Record<string, unknown>;
|
|
65
77
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAuCA,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;IAC5B,6EAA6E;IAC7E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAYD,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMrD;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAWtE;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe;;;;CAIlB,CAAC;AAEX,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,eAAe,GAAG,MAAM,GAAG,IAAI,CAQhG;AAwCD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAKzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAK7G;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,EAAE,CAOjG;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAkBtE;AAeD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBzE"}
|