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/README.md +23 -1
- 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 -441
- 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/safety.js
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/safety.js — Forbidden Patterns, Dangerous Commands, Confirm Logic
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const { C } = require('./ui');
|
|
7
|
-
|
|
8
|
-
const FORBIDDEN_PATTERNS = [
|
|
9
|
-
/rm\s+-rf\s+\/(?:\s|$)/,
|
|
10
|
-
/rm\s+-rf\s+~(?:\/|\s|$)/,
|
|
11
|
-
/rm\s+-rf\s+\.(?:\/|\s|$)/,
|
|
12
|
-
/rm\s+-rf\s+\*(?:\s|$)/,
|
|
13
|
-
/mkfs/,
|
|
14
|
-
/dd\s+if=/,
|
|
15
|
-
/:\(\)\s*\{/,
|
|
16
|
-
/>\/dev\/sd/,
|
|
17
|
-
/curl.*\|\s*(?:ba)?sh/,
|
|
18
|
-
/wget.*\|\s*(?:ba)?sh/,
|
|
19
|
-
/cat\s+.*\.env\b/,
|
|
20
|
-
/cat\s+.*credentials/i,
|
|
21
|
-
/chmod\s+777/,
|
|
22
|
-
/chown\s+root/,
|
|
23
|
-
/passwd/,
|
|
24
|
-
/userdel/,
|
|
25
|
-
/useradd/,
|
|
26
|
-
/\beval\s*\(/,
|
|
27
|
-
/base64.*\|.*bash/,
|
|
28
|
-
// Environment variable exposure
|
|
29
|
-
/\bprintenv\b/,
|
|
30
|
-
// SSH key access
|
|
31
|
-
/cat\s+.*\.ssh\/id_/,
|
|
32
|
-
/cat\s+.*\.ssh\/config/,
|
|
33
|
-
// Reverse shells
|
|
34
|
-
/\bnc\s+-[el]/,
|
|
35
|
-
/\bncat\b/,
|
|
36
|
-
/\bsocat\b/,
|
|
37
|
-
// Indirect code execution
|
|
38
|
-
/python3?\s+-c\s/,
|
|
39
|
-
/node\s+-e\s/,
|
|
40
|
-
/perl\s+-e\s/,
|
|
41
|
-
/ruby\s+-e\s/,
|
|
42
|
-
// History access — only standalone `history` command (not `dnf history`, `git log --history`, etc.)
|
|
43
|
-
/(?:^|[;&|]\s*)history(?:\s|$)/,
|
|
44
|
-
// Data exfiltration via POST
|
|
45
|
-
/curl.*-X\s*POST/,
|
|
46
|
-
/curl.*--data/,
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
// Read-only SSH patterns that are safe (no confirmation needed)
|
|
50
|
-
const SSH_SAFE_PATTERNS = [
|
|
51
|
-
/systemctl\s+(status|is-active|is-enabled|list-units|show)/,
|
|
52
|
-
/journalctl\b/,
|
|
53
|
-
/\btail\s/,
|
|
54
|
-
/\bcat\s/,
|
|
55
|
-
/\bhead\s/,
|
|
56
|
-
/\bls\b/,
|
|
57
|
-
/\bfind\s/,
|
|
58
|
-
/\bgrep\s/,
|
|
59
|
-
/\bwc\s/,
|
|
60
|
-
/\bdf\b/,
|
|
61
|
-
/\bfree\b/,
|
|
62
|
-
/\buptime\b/,
|
|
63
|
-
/\bwho\b/,
|
|
64
|
-
/\bps\s/,
|
|
65
|
-
/\bgit\s+(status|log|diff|branch|fetch)\b/,
|
|
66
|
-
/\bgit\s+pull\b/,
|
|
67
|
-
// Server diagnostics
|
|
68
|
-
/\bss\s+-[tlnp]/,
|
|
69
|
-
/\bnetstat\s/,
|
|
70
|
-
/\bdu\s/,
|
|
71
|
-
/\blscpu\b/,
|
|
72
|
-
/\bnproc\b/,
|
|
73
|
-
/\buname\b/,
|
|
74
|
-
/\bhostname\b/,
|
|
75
|
-
/\bgetent\b/,
|
|
76
|
-
/\bid\b/,
|
|
77
|
-
// Database read-only
|
|
78
|
-
/psql\s.*-c\s/,
|
|
79
|
-
/\bmysql\s.*-e\s/,
|
|
80
|
-
// Package manager read-only
|
|
81
|
-
/\bdnf\s+(check-update|list|info|history|repolist|updateinfo)\b/,
|
|
82
|
-
/\brpm\s+-q/,
|
|
83
|
-
/\bapt\s+list\b/,
|
|
84
|
-
// SSL / Certificates
|
|
85
|
-
/\bopenssl\s+s_client\b/,
|
|
86
|
-
/\bopenssl\s+x509\b/,
|
|
87
|
-
/\bcertbot\s+certificates\b/,
|
|
88
|
-
// Networking read-only
|
|
89
|
-
/\bcurl\s+-[sIkv]|curl\s+--head/,
|
|
90
|
-
/\bdig\s/,
|
|
91
|
-
/\bnslookup\s/,
|
|
92
|
-
/\bping\s/,
|
|
93
|
-
// System info
|
|
94
|
-
/\bgetenforce\b/,
|
|
95
|
-
/\bsesearch\b/,
|
|
96
|
-
/\bausearch\b/,
|
|
97
|
-
/\bsealert\b/,
|
|
98
|
-
/\bcrontab\s+-l\b/,
|
|
99
|
-
/\btimedatectl\b/,
|
|
100
|
-
/\bfirewall-cmd\s+--list/,
|
|
101
|
-
/\bfirewall-cmd\s+--state/,
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
function isSSHReadOnly(command) {
|
|
105
|
-
// Extract the remote command from ssh ... "remote command"
|
|
106
|
-
const remoteCmd = command.match(/ssh\s+[^"]*"([^"]+)"/)?.[1] ||
|
|
107
|
-
command.match(/ssh\s+[^']*'([^']+)'/)?.[1];
|
|
108
|
-
if (!remoteCmd) return false;
|
|
109
|
-
|
|
110
|
-
// Collapse for/while loops into single tokens before splitting on && ;
|
|
111
|
-
// This prevents "for x in a; do cmd; done" from being split incorrectly
|
|
112
|
-
const collapsed = remoteCmd.replace(/\bfor\s[\s\S]*?\bdone\b/g, m => m.replace(/;/g, '\x00'))
|
|
113
|
-
.replace(/\bwhile\s[\s\S]*?\bdone\b/g, m => m.replace(/;/g, '\x00'));
|
|
114
|
-
|
|
115
|
-
// Split compound commands on && ; and check each part
|
|
116
|
-
const parts = collapsed.split(/\s*(?:&&|;)\s*/)
|
|
117
|
-
.map(s => s.replace(/\x00/g, ';').trim())
|
|
118
|
-
.filter(Boolean);
|
|
119
|
-
|
|
120
|
-
// If there are no parts, not safe
|
|
121
|
-
if (parts.length === 0) return false;
|
|
122
|
-
|
|
123
|
-
const isSafePart = (part) => {
|
|
124
|
-
// Strip sudo prefix: -u/-g/-C/-D take an argument, other flags don't
|
|
125
|
-
const cleaned = part.replace(/^sudo\s+(?:-[ugCD]\s+\S+\s+|-[A-Za-z]+\s+)*/, '');
|
|
126
|
-
// Allow echo/printf (output helpers)
|
|
127
|
-
if (/^\s*(?:echo|printf)\s/.test(cleaned)) return true;
|
|
128
|
-
// Allow for/while loops — check the loop body
|
|
129
|
-
if (/^\s*for\s/.test(part) || /^\s*while\s/.test(part)) {
|
|
130
|
-
// Extract the do...done body and check inner commands
|
|
131
|
-
const body = part.match(/\bdo\s+([\s\S]*?)\s*(?:done|$)/)?.[1];
|
|
132
|
-
if (body) {
|
|
133
|
-
const innerParts = body.split(/\s*;\s*/).map(s => s.trim()).filter(Boolean);
|
|
134
|
-
return innerParts.every(ip => isSafePart(ip));
|
|
135
|
-
}
|
|
136
|
-
// If we can't parse the body, check if any safe pattern matches anywhere in the loop
|
|
137
|
-
return SSH_SAFE_PATTERNS.some(pat => pat.test(part));
|
|
138
|
-
}
|
|
139
|
-
// Allow variable assignments
|
|
140
|
-
if (/^\w+=\$?\(/.test(cleaned) || /^\w+=["']/.test(cleaned) || /^\w+=\S/.test(cleaned)) return true;
|
|
141
|
-
return SSH_SAFE_PATTERNS.some(pat => pat.test(cleaned));
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
return parts.every(isSafePart);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const DANGEROUS_BASH = [
|
|
148
|
-
/git\s+push/,
|
|
149
|
-
/npm\s+publish/,
|
|
150
|
-
/npx\s+.*publish/,
|
|
151
|
-
/rm\s+-rf\s/,
|
|
152
|
-
/docker\s+rm/,
|
|
153
|
-
/docker\s+system\s+prune/,
|
|
154
|
-
/kubectl\s+delete/,
|
|
155
|
-
/sudo\s/,
|
|
156
|
-
/ssh\s/,
|
|
157
|
-
/wget\s/,
|
|
158
|
-
/curl\s.*-o\s/,
|
|
159
|
-
/pip\s+install/,
|
|
160
|
-
/npm\s+install\s+-g/,
|
|
161
|
-
];
|
|
162
|
-
|
|
163
|
-
let autoConfirm = false;
|
|
164
|
-
let _rl = null;
|
|
165
|
-
|
|
166
|
-
function setAutoConfirm(val) {
|
|
167
|
-
autoConfirm = val;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function getAutoConfirm() {
|
|
171
|
-
return autoConfirm;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function setReadlineInterface(rl) {
|
|
175
|
-
_rl = rl;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function isForbidden(command) {
|
|
179
|
-
for (const pat of FORBIDDEN_PATTERNS) {
|
|
180
|
-
if (pat.test(command)) return pat;
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function isDangerous(command) {
|
|
186
|
-
// SSH read-only commands are safe — skip confirmation
|
|
187
|
-
if (/ssh\s/.test(command) && isSSHReadOnly(command)) return false;
|
|
188
|
-
for (const pat of DANGEROUS_BASH) {
|
|
189
|
-
if (pat.test(command)) return true;
|
|
190
|
-
}
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* @param {string} question
|
|
196
|
-
* @param {{ toolName?: string }} [opts]
|
|
197
|
-
* @returns {Promise<boolean>}
|
|
198
|
-
*
|
|
199
|
-
* Accepts: y / Enter (default yes) / a (always allow tool) / n
|
|
200
|
-
*/
|
|
201
|
-
function confirm(question, opts = {}) {
|
|
202
|
-
if (autoConfirm) return Promise.resolve(true);
|
|
203
|
-
const hint = opts.toolName ? '[Y/n/a] ' : '[Y/n] ';
|
|
204
|
-
return new Promise((resolve) => {
|
|
205
|
-
const handler = (answer) => {
|
|
206
|
-
const a = answer.trim().toLowerCase();
|
|
207
|
-
if (a === 'a' && opts.toolName) {
|
|
208
|
-
_onAllowAlways(opts.toolName);
|
|
209
|
-
resolve(true);
|
|
210
|
-
} else {
|
|
211
|
-
resolve(a !== 'n');
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
if (_rl) {
|
|
215
|
-
_rl.question(`${C.yellow}${question} ${hint}${C.reset}`, handler);
|
|
216
|
-
} else {
|
|
217
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
218
|
-
rl.question(`${C.yellow}${question} ${hint}${C.reset}`, (answer) => {
|
|
219
|
-
rl.close();
|
|
220
|
-
handler(answer);
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
let _onAllowAlways = () => {}; // set from outside
|
|
227
|
-
function setAllowAlwaysHandler(fn) { _onAllowAlways = fn; }
|
|
228
|
-
|
|
229
|
-
module.exports = {
|
|
230
|
-
FORBIDDEN_PATTERNS,
|
|
231
|
-
SSH_SAFE_PATTERNS,
|
|
232
|
-
isSSHReadOnly,
|
|
233
|
-
DANGEROUS_BASH,
|
|
234
|
-
isForbidden,
|
|
235
|
-
isDangerous,
|
|
236
|
-
confirm,
|
|
237
|
-
setAutoConfirm,
|
|
238
|
-
getAutoConfirm,
|
|
239
|
-
setReadlineInterface,
|
|
240
|
-
setAllowAlwaysHandler,
|
|
241
|
-
};
|
package/cli/session.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/session.js — Session Persistence
|
|
3
|
-
* Save/load conversation sessions to .nex/sessions/
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
function getSessionsDir() {
|
|
10
|
-
return path.join(process.cwd(), '.nex', 'sessions');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function ensureDir() {
|
|
14
|
-
const dir = getSessionsDir();
|
|
15
|
-
if (!fs.existsSync(dir)) {
|
|
16
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Generate a session filename from a name or timestamp
|
|
22
|
-
*/
|
|
23
|
-
function sessionPath(name) {
|
|
24
|
-
const safe = name.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 100);
|
|
25
|
-
return path.join(getSessionsDir(), `${safe}.json`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Save a session to disk
|
|
30
|
-
* @param {string} name — Session name
|
|
31
|
-
* @param {Array} messages — Conversation messages
|
|
32
|
-
* @param {object} [meta] — Additional metadata (model, provider, etc.)
|
|
33
|
-
* @returns {{ path: string, name: string }}
|
|
34
|
-
*/
|
|
35
|
-
function saveSession(name, messages, meta = {}) {
|
|
36
|
-
ensureDir();
|
|
37
|
-
const filePath = sessionPath(name);
|
|
38
|
-
const session = {
|
|
39
|
-
name,
|
|
40
|
-
createdAt: meta.createdAt || new Date().toISOString(),
|
|
41
|
-
updatedAt: new Date().toISOString(),
|
|
42
|
-
messageCount: messages.length,
|
|
43
|
-
model: meta.model || null,
|
|
44
|
-
provider: meta.provider || null,
|
|
45
|
-
messages,
|
|
46
|
-
};
|
|
47
|
-
fs.writeFileSync(filePath, JSON.stringify(session, null, 2), 'utf-8');
|
|
48
|
-
return { path: filePath, name };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Load a session from disk
|
|
53
|
-
* @param {string} name — Session name
|
|
54
|
-
* @returns {object|null} — Session data or null if not found
|
|
55
|
-
*/
|
|
56
|
-
function loadSession(name) {
|
|
57
|
-
const filePath = sessionPath(name);
|
|
58
|
-
if (!fs.existsSync(filePath)) return null;
|
|
59
|
-
try {
|
|
60
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* List all saved sessions, sorted by updatedAt (newest first)
|
|
68
|
-
* @returns {Array<{ name, createdAt, updatedAt, messageCount }>}
|
|
69
|
-
*/
|
|
70
|
-
function listSessions() {
|
|
71
|
-
ensureDir();
|
|
72
|
-
const dir = getSessionsDir();
|
|
73
|
-
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
74
|
-
const sessions = [];
|
|
75
|
-
for (const f of files) {
|
|
76
|
-
try {
|
|
77
|
-
const data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
|
|
78
|
-
sessions.push({
|
|
79
|
-
name: data.name || f.replace('.json', ''),
|
|
80
|
-
createdAt: data.createdAt,
|
|
81
|
-
updatedAt: data.updatedAt,
|
|
82
|
-
messageCount: data.messageCount || 0,
|
|
83
|
-
model: data.model,
|
|
84
|
-
provider: data.provider,
|
|
85
|
-
});
|
|
86
|
-
} catch {
|
|
87
|
-
// skip corrupt files
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return sessions.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Delete a session
|
|
95
|
-
* @param {string} name
|
|
96
|
-
* @returns {boolean}
|
|
97
|
-
*/
|
|
98
|
-
function deleteSession(name) {
|
|
99
|
-
const filePath = sessionPath(name);
|
|
100
|
-
if (!fs.existsSync(filePath)) return false;
|
|
101
|
-
fs.unlinkSync(filePath);
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Get the most recent session (for /resume)
|
|
107
|
-
* @returns {object|null}
|
|
108
|
-
*/
|
|
109
|
-
function getLastSession() {
|
|
110
|
-
const sessions = listSessions();
|
|
111
|
-
if (sessions.length === 0) return null;
|
|
112
|
-
return loadSession(sessions[0].name);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Auto-save the current session (called after each turn)
|
|
117
|
-
* Uses a fixed name '_autosave' that gets overwritten each time
|
|
118
|
-
*/
|
|
119
|
-
function autoSave(messages, meta = {}) {
|
|
120
|
-
if (messages.length === 0) return;
|
|
121
|
-
saveSession('_autosave', messages, meta);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
module.exports = {
|
|
125
|
-
saveSession,
|
|
126
|
-
loadSession,
|
|
127
|
-
listSessions,
|
|
128
|
-
deleteSession,
|
|
129
|
-
getLastSession,
|
|
130
|
-
autoSave,
|
|
131
|
-
// exported for testing
|
|
132
|
-
_getSessionsDir: getSessionsDir,
|
|
133
|
-
};
|