@nexrall/code-core 1.4.67 → 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.
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.API_BASE = void 0;
6
+ exports.SCHEDULE_MAX_MINUTES = exports.SCHEDULE_MIN_MINUTES = exports.API_BASE = void 0;
7
7
  exports.chooseFinalContent = chooseFinalContent;
8
8
  exports.earlyToolResolveEnabled = earlyToolResolveEnabled;
9
9
  exports.gzipRequestsEnabled = gzipRequestsEnabled;
@@ -20,6 +20,12 @@ exports.getUsageDaily = getUsageDaily;
20
20
  exports.exchangeVscodeCode = exchangeVscodeCode;
21
21
  exports.login = login;
22
22
  exports.getUserProfile = getUserProfile;
23
+ exports.clampScheduleMinutes = clampScheduleMinutes;
24
+ exports.listCloudSchedules = listCloudSchedules;
25
+ exports.createCloudSchedule = createCloudSchedule;
26
+ exports.updateCloudSchedule = updateCloudSchedule;
27
+ exports.deleteCloudSchedule = deleteCloudSchedule;
28
+ exports.getCloudScheduleRuns = getCloudScheduleRuns;
23
29
  const eventsource_parser_1 = require("eventsource-parser");
24
30
  const node_fetch_1 = __importDefault(require("node-fetch"));
25
31
  const crypto_1 = require("crypto");
@@ -1982,4 +1988,153 @@ async function getUserProfile() {
1982
1988
  }
1983
1989
  return (await response.json());
1984
1990
  }
1991
+ // ─── Cloud schedules ────────────────────────────────────────────────────────
1992
+ //
1993
+ // Recurring agent tasks that run on NEXRALL'S servers (routes/browserAgent.js
1994
+ // `/api/browser-agent/schedules`, picked up every 60s by services/agentTaskCron),
1995
+ // so the user can close the laptop and the schedule still fires — NOT the same
1996
+ // thing as the local `/loop` + cron tools, which live and die with the session.
1997
+ //
1998
+ // Wire-format notes that decide the code below:
1999
+ // • ids are sharded BIGINTs (migrations 048/080) that exceed
2000
+ // Number.MAX_SAFE_INTEGER. pg returns them as strings, the server's own
2001
+ // routes validate /^\d+$/ and pass the string straight back to pg — so an
2002
+ // id is a digit STRING from end to end here. A Number()/parseInt anywhere on
2003
+ // this path would round to the wrong row (the exact bug class the server
2004
+ // route comments about).
2005
+ // • the server clamps intervals to 15 minutes – 7 days (clampInterval in
2006
+ // routes/browserAgent.js); we validate client-side too, so callers can show
2007
+ // the real rule instead of relaying a 400.
2008
+ // • rows come back snake_case (raw pg). We expose camelCase, one stable shape
2009
+ // for every caller, so no caller has to know column names.
2010
+ exports.SCHEDULE_MIN_MINUTES = 15;
2011
+ exports.SCHEDULE_MAX_MINUTES = 7 * 24 * 60;
2012
+ /** Enforce the server's own bounds with a message that names them. */
2013
+ function clampScheduleMinutes(v) {
2014
+ const n = Math.round(Number(v));
2015
+ if (!Number.isFinite(n) || n < exports.SCHEDULE_MIN_MINUTES || n > exports.SCHEDULE_MAX_MINUTES) {
2016
+ throw new Error(`Interval must be between ${exports.SCHEDULE_MIN_MINUTES} minutes and ${exports.SCHEDULE_MAX_MINUTES / (24 * 60)} days.`);
2017
+ }
2018
+ return n;
2019
+ }
2020
+ /**
2021
+ * A schedule id, still a digit string. Validated here because a malformed id
2022
+ * would otherwise be interpolated into the request path.
2023
+ */
2024
+ function scheduleId(id) {
2025
+ const s = String(id ?? '').trim();
2026
+ if (!/^\d+$/.test(s)) {
2027
+ throw new Error(`Invalid schedule id "${id}" — expected the digit string shown in the schedule list.`);
2028
+ }
2029
+ return s;
2030
+ }
2031
+ function isoOrNull(v) {
2032
+ return typeof v === 'string' && v ? v : null;
2033
+ }
2034
+ /** One shape for a list row, a create response or a partial patch response. */
2035
+ function scheduleRow(row) {
2036
+ return {
2037
+ // String(), never Number() — see the header note on sharded ids.
2038
+ id: row.id == null ? '' : String(row.id),
2039
+ task: typeof row.task === 'string' ? row.task : '',
2040
+ startUrl: isoOrNull(row.start_url ?? row.startUrl),
2041
+ domains: Array.isArray(row.domains) ? row.domains.map((d) => String(d)) : null,
2042
+ intervalMinutes: Number(row.interval_minutes ?? row.intervalMinutes ?? 0) || 0,
2043
+ enabled: row.enabled === true,
2044
+ nextRunAt: isoOrNull(row.next_run_at ?? row.nextRunAt),
2045
+ lastRunAt: isoOrNull(row.last_run_at ?? row.lastRunAt),
2046
+ lastStatus: isoOrNull(row.last_status ?? row.lastStatus),
2047
+ };
2048
+ }
2049
+ function scheduleRunRow(row) {
2050
+ return {
2051
+ id: row.id == null ? '' : String(row.id),
2052
+ status: typeof row.status === 'string' ? row.status : '',
2053
+ summary: typeof row.summary === 'string' && row.summary ? row.summary : null,
2054
+ steps: Number(row.steps ?? 0) || 0,
2055
+ createdAt: isoOrNull(row.created_at ?? row.createdAt),
2056
+ };
2057
+ }
2058
+ /** The server answers errors as {error: '<sentence>'}; prefer that sentence. */
2059
+ function apiErrorMessage(status, text) {
2060
+ try {
2061
+ const j = JSON.parse(text);
2062
+ if (typeof j.error === 'string' && j.error)
2063
+ return j.error;
2064
+ }
2065
+ catch { /* not JSON — fall through to the raw text */ }
2066
+ return `API error ${status}${text ? `: ${text}` : ''}`;
2067
+ }
2068
+ async function scheduleRequest(method, path, body) {
2069
+ const fetchOnce = () => (0, node_fetch_1.default)(`${exports.API_BASE}/api/browser-agent${path}`, {
2070
+ method,
2071
+ headers: authHeaders(),
2072
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
2073
+ });
2074
+ let response = await fetchOnce();
2075
+ // Same silent-refresh pattern as getBalance/getOrgs — an expired token on the
2076
+ // first call of a session is the common case, not an error to surface.
2077
+ if (response.status === 401 || response.status === 403) {
2078
+ const text = await response.text().catch(() => '');
2079
+ if (isExpiredTokenResponse(response.status, text) && await refreshAccessToken()) {
2080
+ response = await fetchOnce();
2081
+ }
2082
+ else {
2083
+ throw new Error(apiErrorMessage(response.status, text));
2084
+ }
2085
+ }
2086
+ if (!response.ok) {
2087
+ throw new Error(apiErrorMessage(response.status, await response.text().catch(() => '')));
2088
+ }
2089
+ return response.json().catch(() => null);
2090
+ }
2091
+ async function listCloudSchedules() {
2092
+ const json = (await scheduleRequest('GET', '/schedules'));
2093
+ return Array.isArray(json?.schedules)
2094
+ ? json.schedules.map((r) => scheduleRow(r))
2095
+ : [];
2096
+ }
2097
+ async function createCloudSchedule(input) {
2098
+ const task = String(input.task ?? '').trim();
2099
+ if (!task)
2100
+ throw new Error('A schedule needs a task.');
2101
+ const json = (await scheduleRequest('POST', '/schedules', {
2102
+ task,
2103
+ intervalMinutes: clampScheduleMinutes(input.intervalMinutes),
2104
+ ...(input.startUrl != null && input.startUrl !== '' ? { startUrl: input.startUrl } : {}),
2105
+ ...(Array.isArray(input.domains) && input.domains.length ? { domains: input.domains } : {}),
2106
+ ...(input.enabled === false ? { enabled: false } : {}),
2107
+ }));
2108
+ if (!json?.schedule)
2109
+ throw new Error('The server accepted the schedule but returned no record.');
2110
+ return scheduleRow(json.schedule);
2111
+ }
2112
+ async function updateCloudSchedule(id, patch) {
2113
+ const body = {};
2114
+ if (typeof patch.task === 'string' && patch.task.trim())
2115
+ body.task = patch.task.trim();
2116
+ if (patch.intervalMinutes != null)
2117
+ body.intervalMinutes = clampScheduleMinutes(patch.intervalMinutes);
2118
+ if (typeof patch.enabled === 'boolean')
2119
+ body.enabled = patch.enabled;
2120
+ if (typeof patch.startUrl === 'string')
2121
+ body.startUrl = patch.startUrl;
2122
+ if (Array.isArray(patch.domains))
2123
+ body.domains = patch.domains;
2124
+ if (!Object.keys(body).length)
2125
+ throw new Error('Nothing to update.');
2126
+ const json = (await scheduleRequest('PATCH', `/schedules/${scheduleId(id)}`, body));
2127
+ if (!json?.schedule)
2128
+ throw new Error('The server accepted the update but returned no record.');
2129
+ return scheduleRow(json.schedule);
2130
+ }
2131
+ async function deleteCloudSchedule(id) {
2132
+ await scheduleRequest('DELETE', `/schedules/${scheduleId(id)}`);
2133
+ }
2134
+ async function getCloudScheduleRuns(id) {
2135
+ const json = (await scheduleRequest('GET', `/schedules/${scheduleId(id)}/runs`));
2136
+ return Array.isArray(json?.runs)
2137
+ ? json.runs.map((r) => scheduleRunRow(r))
2138
+ : [];
2139
+ }
1985
2140
  //# sourceMappingURL=client.js.map
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export * from './agent/skills';
18
18
  export * from './mcp/client';
19
19
  export * from './mcp/httpClient';
20
20
  export * from './mcp/manager';
21
+ export * from './mcp/server';
21
22
  export * from './mcp/stats';
22
23
  export * from './checkpoint/manager';
23
24
  export * from './commands/loader';
@@ -32,6 +33,7 @@ export * from './permissions/destructive';
32
33
  export * from './plugins/index';
33
34
  export * from './plugins/installer';
34
35
  export * from './plugins/sources';
36
+ export * from './plugins/data';
35
37
  export * from './agent/trust';
36
38
  export * from './agent/crossProcessLock';
37
39
  export * from './agent/worktree';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ __exportStar(require("./agent/skills"), exports);
34
34
  __exportStar(require("./mcp/client"), exports);
35
35
  __exportStar(require("./mcp/httpClient"), exports);
36
36
  __exportStar(require("./mcp/manager"), exports);
37
+ __exportStar(require("./mcp/server"), exports);
37
38
  __exportStar(require("./mcp/stats"), exports);
38
39
  __exportStar(require("./checkpoint/manager"), exports);
39
40
  __exportStar(require("./commands/loader"), exports);
@@ -48,6 +49,7 @@ __exportStar(require("./permissions/destructive"), exports);
48
49
  __exportStar(require("./plugins/index"), exports);
49
50
  __exportStar(require("./plugins/installer"), exports);
50
51
  __exportStar(require("./plugins/sources"), exports);
52
+ __exportStar(require("./plugins/data"), exports);
51
53
  __exportStar(require("./agent/trust"), exports);
52
54
  __exportStar(require("./agent/crossProcessLock"), exports);
53
55
  __exportStar(require("./agent/worktree"), exports);
@@ -0,0 +1,39 @@
1
+ import type { Readable, Writable } from 'stream';
2
+ import type { ToolResult } from '../types';
3
+ export interface McpServerTool {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: {
7
+ type: 'object';
8
+ properties?: Record<string, unknown>;
9
+ required?: string[];
10
+ };
11
+ run(input: Record<string, unknown>, ctx: {
12
+ workDir: string;
13
+ }): Promise<ToolResult>;
14
+ }
15
+ export interface ServeOptions {
16
+ /** Workspace root relative paths resolve against (the client's project). */
17
+ workDir: string;
18
+ /** Reported in `serverInfo` during initialize. */
19
+ version?: string;
20
+ /** Defaults to `readOnlyMcpTools()`. */
21
+ tools?: McpServerTool[];
22
+ /** Injectable for tests; default process.stdin/stdout. */
23
+ input?: Readable;
24
+ output?: Writable;
25
+ /** Diagnostics — stderr by convention. NEVER stdout. */
26
+ log?: (message: string) => void;
27
+ }
28
+ /**
29
+ * The read-only surface `nex mcp serve` offers. Kept small on purpose: every tool here is one
30
+ * an outside agent can call with no human in the loop, so nothing that writes, deletes or
31
+ * executes may join without a permission story first.
32
+ */
33
+ export declare function readOnlyMcpTools(): McpServerTool[];
34
+ /**
35
+ * Serve MCP on stdio until the client closes the pipe. Resolves (never rejects) at EOF — the
36
+ * standard way an MCP client says "stop" is to close stdin, and the exit code is then 0.
37
+ */
38
+ export declare function serveMcp(opts: ServeOptions): Promise<void>;
39
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI3C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;IACF,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;IACxB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,wDAAwD;IACxD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAaD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,EAAE,CA+BlD;AASD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuHhE"}
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ // MCP server — the other direction of `mcp/client.ts`.
3
+ //
4
+ // Point any MCP client at `nex mcp serve` and it can read this machine's files with the same
5
+ // tools the CLI uses. Deliberately READ-ONLY for now: the client is usually another agent with
6
+ // its own permission model, and a server that silently edits files has no one to ask — a write
7
+ // toolset only belongs here once there is a person (or a policy file) behind the approval.
8
+ //
9
+ // Framing is the same newline-delimited JSON-RPC 2.0 the client parses, so our own client is
10
+ // the natural test driver. Two hard rules, both enforced by tests:
11
+ // * stdout carries protocol frames and NOTHING else — one stray console.log corrupts the
12
+ // channel; every diagnostic goes to stderr;
13
+ // * a bad line, an unknown method or a throwing tool must never end the loop.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.readOnlyMcpTools = readOnlyMcpTools;
16
+ exports.serveMcp = serveMcp;
17
+ const executor_1 = require("../tools/executor");
18
+ // ─── The toolset ──────────────────────────────────────────────────────────────
19
+ /** Wrap one executor tool. The executor is the single source of behaviour — same caps, same
20
+ * error messages, same path resolution as the CLI's own turns. */
21
+ const fromExecutor = (name, description, inputSchema) => ({
22
+ name,
23
+ description,
24
+ inputSchema,
25
+ run: (input, ctx) => (0, executor_1.executeTool)(name, input, undefined, undefined, ctx.workDir),
26
+ });
27
+ /**
28
+ * The read-only surface `nex mcp serve` offers. Kept small on purpose: every tool here is one
29
+ * an outside agent can call with no human in the loop, so nothing that writes, deletes or
30
+ * executes may join without a permission story first.
31
+ */
32
+ function readOnlyMcpTools() {
33
+ return [
34
+ fromExecutor('read_file', 'Read a text file. Returns numbered lines so later edits can cite them. Binary files are refused.', { type: 'object', properties: {
35
+ path: { type: 'string', description: 'File path, absolute or relative to the workspace root' },
36
+ offset: { type: 'number', description: '0-based line to start from (default 0)' },
37
+ limit: { type: 'number', description: 'How many lines to return' },
38
+ }, required: ['path'] }),
39
+ fromExecutor('list_directory', 'List the entries of a directory, each marked as file or directory.', { type: 'object', properties: {
40
+ path: { type: 'string', description: 'Directory path (default: the workspace root)' },
41
+ } }),
42
+ fromExecutor('glob', 'Find files by glob pattern, e.g. "**/*.ts" or "src/**/*.test.js".', { type: 'object', properties: {
43
+ pattern: { type: 'string', description: 'Glob pattern' },
44
+ path: { type: 'string', description: 'Directory to search from (default: the workspace root)' },
45
+ }, required: ['pattern'] }),
46
+ fromExecutor('search_files', 'Search file contents with a regular expression. Returns matching file paths by default; ask for context lines to get the matches themselves.', { type: 'object', properties: {
47
+ pattern: { type: 'string', description: 'Regular expression' },
48
+ path: { type: 'string', description: 'Directory to search (default: the workspace root)' },
49
+ type: { type: 'string', description: '"files" (paths only) or "content" (matching lines)' },
50
+ include: { type: 'string', description: 'Only files matching this glob, e.g. "*.ts"' },
51
+ case_insensitive: { type: 'boolean', description: 'Case-insensitive match' },
52
+ context_lines: { type: 'number', description: 'Lines of context around each match (implies type "content")' },
53
+ }, required: ['pattern'] }),
54
+ ];
55
+ }
56
+ // ─── Protocol ─────────────────────────────────────────────────────────────────
57
+ /** Versions whose framing we actually implement (they are all the same for our surface). */
58
+ const SUPPORTED_PROTOCOLS = ['2024-11-05', '2025-03-26', '2025-06-18'];
59
+ const DEFAULT_PROTOCOL = '2024-11-05';
60
+ const SERVER_NAME = 'nexrall';
61
+ /**
62
+ * Serve MCP on stdio until the client closes the pipe. Resolves (never rejects) at EOF — the
63
+ * standard way an MCP client says "stop" is to close stdin, and the exit code is then 0.
64
+ */
65
+ async function serveMcp(opts) {
66
+ const input = opts.input ?? process.stdin;
67
+ const output = opts.output ?? process.stdout;
68
+ const log = opts.log ?? ((m) => process.stderr.write(`[nexrall mcp] ${m}\n`));
69
+ const tools = opts.tools ?? readOnlyMcpTools();
70
+ const version = opts.version ?? '0.0.0';
71
+ const byName = new Map(tools.map((t) => [t.name, t]));
72
+ const send = (msg) => { output.write(JSON.stringify(msg) + '\n'); };
73
+ const reply = (id, result) => send({ jsonrpc: '2.0', id, result });
74
+ const replyError = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } });
75
+ async function handle(message) {
76
+ const id = message.id;
77
+ const isRequest = id !== undefined && id !== null;
78
+ const method = message.method;
79
+ const params = (message.params ?? {});
80
+ switch (method) {
81
+ case 'initialize': {
82
+ // Echo the client's version when we implement it, else our default — that is the
83
+ // spec's instruction and the number is inert for this surface either way.
84
+ const asked = typeof params.protocolVersion === 'string' ? params.protocolVersion : '';
85
+ reply(id, {
86
+ protocolVersion: SUPPORTED_PROTOCOLS.includes(asked) ? asked : DEFAULT_PROTOCOL,
87
+ capabilities: { tools: {} },
88
+ serverInfo: { name: SERVER_NAME, version },
89
+ instructions: `Read-only file access to ${opts.workDir}. ` +
90
+ `Tools: ${tools.map((t) => t.name).join(', ')}. Nothing here writes, deletes or runs commands.`,
91
+ });
92
+ log(`initialize from ${typeof params.clientInfo === 'object' && params.clientInfo ? JSON.stringify(params.clientInfo.name ?? 'client') : 'client'}`);
93
+ return;
94
+ }
95
+ case 'notifications/initialized':
96
+ case 'notifications/cancelled':
97
+ return; // notifications take no reply
98
+ case 'ping':
99
+ reply(id, {});
100
+ return;
101
+ case 'tools/list':
102
+ reply(id, { tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
103
+ return;
104
+ case 'tools/call': {
105
+ const name = typeof params.name === 'string' ? params.name : '';
106
+ const args = (params.arguments ?? {});
107
+ if (!name)
108
+ return replyError(id, -32602, 'Invalid params: name is required');
109
+ const tool = byName.get(name);
110
+ if (!tool) {
111
+ // A RESULT, not a protocol error: the calling model should read "not offered" as tool
112
+ // output and pick another route, not see the whole call fail.
113
+ reply(id, { content: [{ type: 'text', text: `Tool "${name}" is not offered by this server (read-only). Available: ${[...byName.keys()].join(', ')}.` }], isError: true });
114
+ return;
115
+ }
116
+ try {
117
+ const res = await tool.run(args, { workDir: opts.workDir });
118
+ if (res.error)
119
+ reply(id, { content: [{ type: 'text', text: res.error }], isError: true });
120
+ else
121
+ reply(id, { content: [{ type: 'text', text: res.output ?? '' }] });
122
+ }
123
+ catch (err) {
124
+ // A throwing tool is the tool's problem, not the session's — answer and keep serving.
125
+ reply(id, { content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }], isError: true });
126
+ }
127
+ return;
128
+ }
129
+ default:
130
+ if (isRequest)
131
+ replyError(id, -32601, `Method not found: ${method}`);
132
+ return;
133
+ }
134
+ }
135
+ input.setEncoding('utf8');
136
+ let buffer = '';
137
+ let queue = Promise.resolve();
138
+ const feed = (line) => {
139
+ // Chain instead of racing: responses stay in request order and writes never interleave.
140
+ queue = queue.then(async () => {
141
+ const trimmed = line.trim();
142
+ if (!trimmed)
143
+ return;
144
+ let message;
145
+ try {
146
+ message = JSON.parse(trimmed);
147
+ }
148
+ catch {
149
+ log(`ignoring a line that is not JSON (${trimmed.slice(0, 80)})`);
150
+ return;
151
+ }
152
+ if (!message || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
153
+ log('ignoring a message without jsonrpc 2.0 + method');
154
+ return;
155
+ }
156
+ try {
157
+ await handle(message);
158
+ }
159
+ catch (err) {
160
+ // handle() is written not to throw, so reaching here is a bug in the server — report
161
+ // it as a JSON-RPC error rather than dropping the connection.
162
+ if (message.id !== undefined)
163
+ replyError(message.id, -32603, err instanceof Error ? err.message : String(err));
164
+ }
165
+ });
166
+ };
167
+ await new Promise((resolve) => {
168
+ input.on('data', (chunk) => {
169
+ buffer += chunk.toString();
170
+ let nl;
171
+ while ((nl = buffer.indexOf('\n')) >= 0) {
172
+ feed(buffer.slice(0, nl));
173
+ buffer = buffer.slice(nl + 1);
174
+ }
175
+ });
176
+ const done = () => {
177
+ if (buffer.trim()) {
178
+ feed(buffer);
179
+ buffer = '';
180
+ }
181
+ resolve();
182
+ };
183
+ input.on('end', done);
184
+ input.on('close', done);
185
+ });
186
+ await queue; // let in-flight calls finish before the process exits
187
+ log('client closed stdin — server exiting');
188
+ }
189
+ //# sourceMappingURL=server.js.map
@@ -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
@@ -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":"AAsCA,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,mGAAmG;AACnG,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAkBtE;AAED,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBzE"}
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"}