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/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
- };