nex-code 0.3.4 → 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/README.md +34 -12
- package/dist/bundle.js +505 -0
- package/dist/nex-code.js +485 -0
- package/package.json +8 -6
- package/bin/nex-code.js +0 -99
- package/cli/agent.js +0 -835
- package/cli/compactor.js +0 -85
- package/cli/context-engine.js +0 -507
- package/cli/context.js +0 -98
- package/cli/costs.js +0 -290
- package/cli/diff.js +0 -366
- package/cli/file-history.js +0 -94
- package/cli/format.js +0 -211
- package/cli/fuzzy-match.js +0 -270
- package/cli/git.js +0 -211
- package/cli/hooks.js +0 -173
- package/cli/index.js +0 -1289
- package/cli/mcp.js +0 -284
- package/cli/memory.js +0 -170
- package/cli/ollama.js +0 -130
- package/cli/permissions.js +0 -124
- package/cli/picker.js +0 -201
- package/cli/planner.js +0 -282
- package/cli/providers/anthropic.js +0 -333
- package/cli/providers/base.js +0 -116
- package/cli/providers/gemini.js +0 -239
- package/cli/providers/local.js +0 -249
- package/cli/providers/ollama.js +0 -228
- package/cli/providers/openai.js +0 -237
- package/cli/providers/registry.js +0 -454
- package/cli/render.js +0 -495
- package/cli/safety.js +0 -241
- package/cli/session.js +0 -133
- package/cli/skills.js +0 -412
- package/cli/spinner.js +0 -371
- package/cli/sub-agent.js +0 -425
- package/cli/tasks.js +0 -179
- package/cli/tool-tiers.js +0 -164
- package/cli/tool-validator.js +0 -138
- package/cli/tools.js +0 -1050
- package/cli/ui.js +0 -93
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
|
-
};
|
package/cli/permissions.js
DELETED
|
@@ -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
|
-
};
|