nex-code 0.3.5 → 0.3.7

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/cli/mcp.js DELETED
@@ -1,284 +0,0 @@
1
- /**
2
- * cli/mcp.js — MCP (Model Context Protocol) Client
3
- * Discovers and invokes tools from external MCP servers via JSON-RPC over stdio.
4
- */
5
-
6
- const { spawn } = require('child_process');
7
- const path = require('path');
8
- const fs = require('fs');
9
-
10
- // Active MCP server connections
11
- const activeServers = new Map();
12
-
13
- function getConfigPath() {
14
- return path.join(process.cwd(), '.nex', 'config.json');
15
- }
16
-
17
- /**
18
- * Load MCP server configurations from .nex/config.json
19
- * @returns {Object<string, {command: string, args?: string[], env?: Object}>}
20
- */
21
- function loadMCPConfig() {
22
- const configPath = getConfigPath();
23
- if (!fs.existsSync(configPath)) return {};
24
- try {
25
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
26
- return config.mcpServers || {};
27
- } catch {
28
- return {};
29
- }
30
- }
31
-
32
- /**
33
- * Send a JSON-RPC request to an MCP server process
34
- * @param {import('child_process').ChildProcess} proc
35
- * @param {string} method
36
- * @param {Object} params
37
- * @param {number} timeout — ms
38
- * @returns {Promise<Object>}
39
- */
40
- function sendRequest(proc, method, params = {}, timeout = 10000) {
41
- return new Promise((resolve, reject) => {
42
- const id = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
43
- const request = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
44
-
45
- let buffer = '';
46
- const timer = setTimeout(() => {
47
- cleanup();
48
- reject(new Error(`MCP request timeout: ${method}`));
49
- }, timeout);
50
-
51
- function onData(data) {
52
- buffer += data.toString();
53
- const lines = buffer.split('\n');
54
- for (const line of lines) {
55
- if (!line.trim()) continue;
56
- try {
57
- const msg = JSON.parse(line);
58
- if (msg.id === id) {
59
- cleanup();
60
- if (msg.error) {
61
- reject(new Error(`MCP error: ${msg.error.message || JSON.stringify(msg.error)}`));
62
- } else {
63
- resolve(msg.result);
64
- }
65
- return;
66
- }
67
- } catch {
68
- // Not valid JSON yet, continue buffering
69
- }
70
- }
71
- // Keep only the last incomplete line
72
- buffer = lines[lines.length - 1] || '';
73
- }
74
-
75
- function cleanup() {
76
- clearTimeout(timer);
77
- proc.stdout.removeListener('data', onData);
78
- }
79
-
80
- proc.stdout.on('data', onData);
81
- try {
82
- proc.stdin.write(request);
83
- } catch (e) {
84
- cleanup();
85
- reject(new Error(`MCP write failed: ${e.message}`));
86
- }
87
- });
88
- }
89
-
90
- /**
91
- * Connect to an MCP server
92
- * @param {string} name
93
- * @param {{command: string, args?: string[], env?: Object}} config
94
- * @returns {Promise<{name: string, tools: Array}>}
95
- */
96
- async function connectServer(name, config) {
97
- if (activeServers.has(name)) {
98
- return activeServers.get(name);
99
- }
100
-
101
- // Allowlist safe env vars to prevent API key leakage
102
- const SAFE_ENV_KEYS = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'TERM', 'NODE_ENV'];
103
- const filteredEnv = {};
104
- for (const key of SAFE_ENV_KEYS) {
105
- if (process.env[key]) filteredEnv[key] = process.env[key];
106
- }
107
- const proc = spawn(config.command, config.args || [], {
108
- stdio: ['pipe', 'pipe', 'pipe'],
109
- env: { ...filteredEnv, ...(config.env || {}) },
110
- });
111
-
112
- const server = { name, proc, tools: [], config };
113
-
114
- // Initialize with JSON-RPC
115
- try {
116
- await sendRequest(proc, 'initialize', {
117
- protocolVersion: '2024-11-05',
118
- capabilities: {},
119
- clientInfo: { name: 'nex-code', version: '0.2.0' },
120
- });
121
-
122
- // Discover tools
123
- const toolsResult = await sendRequest(proc, 'tools/list', {});
124
- server.tools = (toolsResult && toolsResult.tools) || [];
125
-
126
- activeServers.set(name, server);
127
- return server;
128
- } catch (err) {
129
- proc.kill();
130
- throw new Error(`Failed to connect MCP server '${name}': ${err.message}`);
131
- }
132
- }
133
-
134
- /**
135
- * Disconnect an MCP server
136
- * @param {string} name
137
- */
138
- function disconnectServer(name) {
139
- const server = activeServers.get(name);
140
- if (!server) return false;
141
- try {
142
- server.proc.kill();
143
- } catch {
144
- // Process may already be dead
145
- }
146
- activeServers.delete(name);
147
- return true;
148
- }
149
-
150
- /**
151
- * Disconnect all MCP servers
152
- */
153
- function disconnectAll() {
154
- for (const [name] of activeServers) {
155
- disconnectServer(name);
156
- }
157
- }
158
-
159
- /**
160
- * Call a tool on an MCP server
161
- * @param {string} serverName
162
- * @param {string} toolName
163
- * @param {Object} args
164
- * @returns {Promise<string>}
165
- */
166
- async function callTool(serverName, toolName, args = {}) {
167
- const server = activeServers.get(serverName);
168
- if (!server) throw new Error(`MCP server not connected: ${serverName}`);
169
-
170
- const result = await sendRequest(server.proc, 'tools/call', {
171
- name: toolName,
172
- arguments: args,
173
- });
174
-
175
- // MCP returns content array — extract text
176
- if (result && Array.isArray(result.content)) {
177
- return result.content
178
- .filter((c) => c.type === 'text')
179
- .map((c) => c.text)
180
- .join('\n');
181
- }
182
- return JSON.stringify(result);
183
- }
184
-
185
- /**
186
- * Get all tools from all connected MCP servers
187
- * @returns {Array<{server: string, name: string, description: string, inputSchema: Object}>}
188
- */
189
- function getAllTools() {
190
- const tools = [];
191
- for (const [serverName, server] of activeServers) {
192
- for (const tool of server.tools) {
193
- tools.push({
194
- server: serverName,
195
- name: tool.name,
196
- description: tool.description || '',
197
- inputSchema: tool.inputSchema || { type: 'object', properties: {} },
198
- });
199
- }
200
- }
201
- return tools;
202
- }
203
-
204
- /**
205
- * Convert MCP tools to OpenAI-style tool definitions for the LLM
206
- * @returns {Array}
207
- */
208
- function getMCPToolDefinitions() {
209
- return getAllTools().map((t) => ({
210
- type: 'function',
211
- function: {
212
- name: `mcp_${t.server}_${t.name}`,
213
- description: `[MCP:${t.server}] ${t.description}`,
214
- parameters: t.inputSchema,
215
- },
216
- }));
217
- }
218
-
219
- /**
220
- * Check if a tool call is an MCP tool and route it
221
- * @param {string} fnName
222
- * @param {Object} args
223
- * @returns {Promise<string|null>} — null if not an MCP tool
224
- */
225
- async function routeMCPCall(fnName, args) {
226
- if (!fnName.startsWith('mcp_')) return null;
227
-
228
- const parts = fnName.substring(4).split('_');
229
- if (parts.length < 2) return null;
230
-
231
- const serverName = parts[0];
232
- const toolName = parts.slice(1).join('_');
233
-
234
- return callTool(serverName, toolName, args);
235
- }
236
-
237
- /**
238
- * List configured MCP servers and their status
239
- * @returns {Array<{name: string, command: string, connected: boolean, toolCount: number}>}
240
- */
241
- function listServers() {
242
- const config = loadMCPConfig();
243
- return Object.entries(config).map(([name, conf]) => {
244
- const server = activeServers.get(name);
245
- return {
246
- name,
247
- command: conf.command,
248
- connected: !!server,
249
- toolCount: server ? server.tools.length : 0,
250
- };
251
- });
252
- }
253
-
254
- /**
255
- * Connect all configured MCP servers
256
- * @returns {Promise<Array<{name: string, tools: number, error?: string}>>}
257
- */
258
- async function connectAll() {
259
- const config = loadMCPConfig();
260
- const results = [];
261
- for (const [name, conf] of Object.entries(config)) {
262
- try {
263
- const server = await connectServer(name, conf);
264
- results.push({ name, tools: server.tools.length });
265
- } catch (err) {
266
- results.push({ name, tools: 0, error: err.message });
267
- }
268
- }
269
- return results;
270
- }
271
-
272
- module.exports = {
273
- loadMCPConfig,
274
- sendRequest,
275
- connectServer,
276
- disconnectServer,
277
- disconnectAll,
278
- callTool,
279
- getAllTools,
280
- getMCPToolDefinitions,
281
- routeMCPCall,
282
- listServers,
283
- connectAll,
284
- };
package/cli/memory.js DELETED
@@ -1,170 +0,0 @@
1
- /**
2
- * cli/memory.js — Project Memory
3
- * Persistent key-value memory stored in .nex/memory/
4
- * Also loads NEX.md from project root for project-level instructions
5
- */
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
- const os = require('os');
10
-
11
- function getMemoryDir() {
12
- return path.join(process.cwd(), '.nex', 'memory');
13
- }
14
-
15
- function getMemoryFile() {
16
- return path.join(getMemoryDir(), 'memory.json');
17
- }
18
-
19
- function getNexMdPath() {
20
- return path.join(process.cwd(), 'NEX.md');
21
- }
22
-
23
- function getGlobalNexMdPath() {
24
- return path.join(os.homedir(), '.nex', 'NEX.md');
25
- }
26
-
27
- function ensureDir() {
28
- const dir = getMemoryDir();
29
- if (!fs.existsSync(dir)) {
30
- fs.mkdirSync(dir, { recursive: true });
31
- }
32
- }
33
-
34
- function readMemoryFile() {
35
- const file = getMemoryFile();
36
- if (!fs.existsSync(file)) return {};
37
- try {
38
- return JSON.parse(fs.readFileSync(file, 'utf-8'));
39
- } catch {
40
- return {};
41
- }
42
- }
43
-
44
- function writeMemoryFile(data) {
45
- ensureDir();
46
- fs.writeFileSync(getMemoryFile(), JSON.stringify(data, null, 2), 'utf-8');
47
- }
48
-
49
- /**
50
- * Remember a key-value pair
51
- * @param {string} key
52
- * @param {string} value
53
- */
54
- function remember(key, value) {
55
- const data = readMemoryFile();
56
- data[key] = {
57
- value,
58
- updatedAt: new Date().toISOString(),
59
- };
60
- writeMemoryFile(data);
61
- }
62
-
63
- /**
64
- * Recall a value by key
65
- * @param {string} key
66
- * @returns {string|null}
67
- */
68
- function recall(key) {
69
- const data = readMemoryFile();
70
- if (data[key]) return data[key].value;
71
- return null;
72
- }
73
-
74
- /**
75
- * Forget (delete) a memory
76
- * @param {string} key
77
- * @returns {boolean}
78
- */
79
- function forget(key) {
80
- const data = readMemoryFile();
81
- if (!(key in data)) return false;
82
- delete data[key];
83
- writeMemoryFile(data);
84
- return true;
85
- }
86
-
87
- /**
88
- * List all memories
89
- * @returns {Array<{ key, value, updatedAt }>}
90
- */
91
- function listMemories() {
92
- const data = readMemoryFile();
93
- return Object.entries(data).map(([key, entry]) => ({
94
- key,
95
- value: entry.value,
96
- updatedAt: entry.updatedAt,
97
- }));
98
- }
99
-
100
- /**
101
- * Load global NEX.md from ~/.nex/NEX.md (if it exists)
102
- * @returns {string} — Contents of global NEX.md or empty string
103
- */
104
- function loadGlobalInstructions() {
105
- const globalMd = getGlobalNexMdPath();
106
- if (!fs.existsSync(globalMd)) return '';
107
- try {
108
- return fs.readFileSync(globalMd, 'utf-8').trim();
109
- } catch {
110
- return '';
111
- }
112
- }
113
-
114
- /**
115
- * Load NEX.md from project root (if it exists)
116
- * @returns {string} — Contents of NEX.md or empty string
117
- */
118
- function loadProjectInstructions() {
119
- const nexMd = getNexMdPath();
120
- if (!fs.existsSync(nexMd)) return '';
121
- try {
122
- return fs.readFileSync(nexMd, 'utf-8').trim();
123
- } catch {
124
- return '';
125
- }
126
- }
127
-
128
- /**
129
- * Get memory context for system prompt inclusion
130
- * Returns formatted string with memories + NEX.md content
131
- * @returns {string}
132
- */
133
- function getMemoryContext() {
134
- const parts = [];
135
-
136
- // Load global NEX.md (~/.nex/NEX.md)
137
- const globalInstructions = loadGlobalInstructions();
138
- if (globalInstructions) {
139
- parts.push(`GLOBAL INSTRUCTIONS (~/.nex/NEX.md):\n${globalInstructions}`);
140
- }
141
-
142
- // Load project NEX.md
143
- const instructions = loadProjectInstructions();
144
- if (instructions) {
145
- parts.push(`PROJECT INSTRUCTIONS (NEX.md):\n${instructions}`);
146
- }
147
-
148
- // Load memories
149
- const memories = listMemories();
150
- if (memories.length > 0) {
151
- const memStr = memories.map((m) => ` ${m.key}: ${m.value}`).join('\n');
152
- parts.push(`PROJECT MEMORY:\n${memStr}`);
153
- }
154
-
155
- return parts.join('\n\n');
156
- }
157
-
158
- module.exports = {
159
- remember,
160
- recall,
161
- forget,
162
- listMemories,
163
- loadGlobalInstructions,
164
- loadProjectInstructions,
165
- getMemoryContext,
166
- // exported for testing
167
- _getMemoryDir: getMemoryDir,
168
- _getMemoryFile: getMemoryFile,
169
- _getGlobalNexMdPath: getGlobalNexMdPath,
170
- };
package/cli/ollama.js DELETED
@@ -1,130 +0,0 @@
1
- /**
2
- * cli/ollama.js — Ollama API Client (Backward-compatible wrapper)
3
- *
4
- * This module now delegates to the provider system (cli/providers/).
5
- * Exports the same API for backward compatibility.
6
- */
7
-
8
- const registry = require('./providers/registry');
9
-
10
- const MODELS = {
11
- 'kimi-k2.5': { id: 'kimi-k2.5', name: 'Kimi K2.5', max_tokens: 16384 },
12
- 'qwen3-coder:480b': { id: 'qwen3-coder:480b', name: 'Qwen3 Coder 480B', max_tokens: 16384 },
13
- };
14
-
15
- function getActiveModel() {
16
- return registry.getActiveModel();
17
- }
18
-
19
- function setActiveModel(name) {
20
- return registry.setActiveModel(name);
21
- }
22
-
23
- function getModelNames() {
24
- return registry.getModelNames();
25
- }
26
-
27
- /**
28
- * Parse tool call arguments with fallback strategies.
29
- * This is a utility function, not provider-specific.
30
- */
31
- function parseToolArgs(raw) {
32
- if (!raw) return null;
33
- if (typeof raw === 'object') return raw;
34
- try {
35
- return JSON.parse(raw);
36
- } catch {
37
- /* continue */
38
- }
39
- try {
40
- const fixed = raw.replace(/,\s*([}\]])/g, '$1').replace(/'/g, '"');
41
- return JSON.parse(fixed);
42
- } catch {
43
- /* continue */
44
- }
45
- const match = raw.match(/\{[\s\S]*\}/);
46
- if (match) {
47
- try {
48
- return JSON.parse(match[0]);
49
- } catch {
50
- /* continue */
51
- }
52
- }
53
-
54
- // Strategy 4: Fix unquoted keys (common in OS models)
55
- try {
56
- const fixedKeys = raw.replace(/(\{|,)\s*([a-zA-Z_]\w*)\s*:/g, '$1"$2":');
57
- return JSON.parse(fixedKeys);
58
- } catch {
59
- /* continue */
60
- }
61
-
62
- // Strategy 5: Strip markdown code fences (DeepSeek R1, Llama wrap JSON in ```json)
63
- const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
64
- if (fenceMatch) {
65
- try {
66
- return JSON.parse(fenceMatch[1].trim());
67
- } catch {
68
- /* give up */
69
- }
70
- }
71
-
72
- return null;
73
- }
74
-
75
- /**
76
- * @deprecated Use providers/registry.callStream() instead.
77
- * Streaming call through the active provider.
78
- */
79
- async function callOllamaStream(messages, tools) {
80
- const { C } = require('./ui');
81
- const { Spinner } = require('./ui');
82
-
83
- const spinner = new Spinner('Thinking...');
84
- spinner.start();
85
- let firstToken = true;
86
- let contentStr = '';
87
-
88
- try {
89
- const result = await registry.callStream(messages, tools, {
90
- onToken: (text) => {
91
- if (firstToken) {
92
- spinner.stop();
93
- process.stdout.write(`${C.blue}`);
94
- firstToken = false;
95
- }
96
- process.stdout.write(text);
97
- contentStr += text;
98
- },
99
- });
100
-
101
- if (firstToken) {
102
- spinner.stop();
103
- } else {
104
- process.stdout.write(`${C.reset}\n`);
105
- }
106
-
107
- return result;
108
- } catch (err) {
109
- spinner.stop();
110
- throw err;
111
- }
112
- }
113
-
114
- /**
115
- * @deprecated Use providers/registry.callChat() instead.
116
- * Non-streaming call through the active provider.
117
- */
118
- async function callOllama(messages, tools) {
119
- return registry.callChat(messages, tools);
120
- }
121
-
122
- module.exports = {
123
- MODELS,
124
- getActiveModel,
125
- setActiveModel,
126
- getModelNames,
127
- callOllamaStream,
128
- callOllama,
129
- parseToolArgs,
130
- };
@@ -1,124 +0,0 @@
1
- /**
2
- * cli/permissions.js — Tool Permission System
3
- * Three modes per tool: 'allow' (auto), 'ask' (confirm), 'deny' (blocked)
4
- */
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const { C } = require('./ui');
9
-
10
- // Default permissions: read ops auto, write/bash ask
11
- const DEFAULT_PERMISSIONS = {
12
- bash: 'ask',
13
- read_file: 'allow',
14
- write_file: 'ask',
15
- edit_file: 'ask',
16
- list_directory: 'allow',
17
- search_files: 'allow',
18
- glob: 'allow',
19
- grep: 'allow',
20
- patch_file: 'ask',
21
- web_fetch: 'allow',
22
- web_search: 'allow',
23
- ask_user: 'allow',
24
- task_list: 'allow',
25
- spawn_agents: 'ask',
26
- };
27
-
28
- let permissions = { ...DEFAULT_PERMISSIONS };
29
-
30
- /**
31
- * Load permissions from .nex/config.json if it exists
32
- */
33
- function loadPermissions() {
34
- const configPath = path.join(process.cwd(), '.nex', 'config.json');
35
- if (!fs.existsSync(configPath)) return;
36
- try {
37
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
38
- if (config.permissions) {
39
- permissions = { ...DEFAULT_PERMISSIONS, ...config.permissions };
40
- }
41
- } catch {
42
- // ignore corrupt config
43
- }
44
- }
45
-
46
- /**
47
- * Save current permissions to .nex/config.json
48
- */
49
- function savePermissions() {
50
- const configDir = path.join(process.cwd(), '.nex');
51
- const configPath = path.join(configDir, 'config.json');
52
-
53
- let config = {};
54
- if (fs.existsSync(configPath)) {
55
- try {
56
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
57
- } catch {
58
- config = {};
59
- }
60
- }
61
-
62
- config.permissions = permissions;
63
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
64
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
65
- }
66
-
67
- /**
68
- * Get permission for a tool
69
- * @param {string} toolName
70
- * @returns {'allow'|'ask'|'deny'}
71
- */
72
- function getPermission(toolName) {
73
- return permissions[toolName] || 'ask';
74
- }
75
-
76
- /**
77
- * Set permission for a tool
78
- * @param {string} toolName
79
- * @param {'allow'|'ask'|'deny'} mode
80
- * @returns {boolean} true if valid
81
- */
82
- function setPermission(toolName, mode) {
83
- if (!['allow', 'ask', 'deny'].includes(mode)) return false;
84
- permissions[toolName] = mode;
85
- return true;
86
- }
87
-
88
- /**
89
- * Check if a tool execution is allowed
90
- * @param {string} toolName
91
- * @returns {'allow'|'ask'|'deny'}
92
- */
93
- function checkPermission(toolName) {
94
- return getPermission(toolName);
95
- }
96
-
97
- /**
98
- * List all permissions
99
- * @returns {Array<{ tool, mode }>}
100
- */
101
- function listPermissions() {
102
- return Object.entries(permissions).map(([tool, mode]) => ({ tool, mode }));
103
- }
104
-
105
- /**
106
- * Reset permissions to defaults
107
- */
108
- function resetPermissions() {
109
- permissions = { ...DEFAULT_PERMISSIONS };
110
- }
111
-
112
- // Load on init
113
- loadPermissions();
114
-
115
- module.exports = {
116
- getPermission,
117
- setPermission,
118
- checkPermission,
119
- listPermissions,
120
- loadPermissions,
121
- savePermissions,
122
- resetPermissions,
123
- DEFAULT_PERMISSIONS,
124
- };