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/picker.js DELETED
@@ -1,201 +0,0 @@
1
- /**
2
- * cli/picker.js — Interactive Terminal Picker
3
- * Generic cursor-based list picker for terminal UIs.
4
- */
5
-
6
- const { C } = require('./ui');
7
- const { listProviders, getActiveProviderName, getActiveModelId, setActiveModel } = require('./providers/registry');
8
-
9
- /**
10
- * Generic interactive list picker.
11
- * @param {readline.Interface} rl - readline instance (will be paused during pick)
12
- * @param {Array<{ label: string, value: string|null, isHeader?: boolean, isCurrent?: boolean }>} items
13
- * @param {object} [options]
14
- * @param {string} [options.title] - Title shown above the list
15
- * @param {string} [options.hint] - Hint shown below title
16
- * @returns {Promise<string|null>} selected value or null if cancelled
17
- */
18
- function pickFromList(rl, items, options = {}) {
19
- const { title = 'Select', hint = '\u2191\u2193 navigate \u00b7 Enter select \u00b7 Esc cancel' } = options;
20
-
21
- return new Promise((resolve) => {
22
- // Find selectable items (non-headers)
23
- const selectableIndices = items
24
- .map((item, i) => (item.isHeader ? -1 : i))
25
- .filter((i) => i >= 0);
26
-
27
- if (selectableIndices.length === 0) {
28
- resolve(null);
29
- return;
30
- }
31
-
32
- // Start cursor at current item, or first selectable
33
- const currentIdx = items.findIndex((item) => item.isCurrent);
34
- let cursor = currentIdx >= 0 ? selectableIndices.indexOf(currentIdx) : 0;
35
- if (cursor < 0) cursor = 0;
36
-
37
- // Calculate visible window for scrolling
38
- const maxVisible = process.stdout.rows ? Math.max(process.stdout.rows - 6, 5) : 20;
39
- let scrollOffset = 0;
40
-
41
- function getVisibleRange() {
42
- // Ensure cursor is visible
43
- const cursorItemIdx = selectableIndices[cursor];
44
- if (cursorItemIdx < scrollOffset) {
45
- scrollOffset = cursorItemIdx;
46
- } else if (cursorItemIdx >= scrollOffset + maxVisible) {
47
- scrollOffset = cursorItemIdx - maxVisible + 1;
48
- }
49
- return { start: scrollOffset, end: Math.min(items.length, scrollOffset + maxVisible) };
50
- }
51
-
52
- let renderedLines = 0;
53
-
54
- function render() {
55
- // Clear previous render
56
- if (renderedLines > 0) {
57
- process.stdout.write(`\x1b[${renderedLines}A`);
58
- for (let i = 0; i < renderedLines; i++) {
59
- process.stdout.write('\x1b[2K\n');
60
- }
61
- process.stdout.write(`\x1b[${renderedLines}A`);
62
- }
63
-
64
- const lines = [];
65
- lines.push(` ${C.bold}${C.cyan}${title}${C.reset}`);
66
- lines.push(` ${C.dim}${hint}${C.reset}`);
67
- lines.push('');
68
-
69
- const { start, end } = getVisibleRange();
70
-
71
- if (start > 0) {
72
- lines.push(` ${C.dim}\u2191 more${C.reset}`);
73
- }
74
-
75
- for (let i = start; i < end; i++) {
76
- const item = items[i];
77
- if (item.isHeader) {
78
- lines.push(` ${C.bold}${C.dim}${item.label}${C.reset}`);
79
- continue;
80
- }
81
-
82
- const isSelected = selectableIndices[cursor] === i;
83
- const pointer = isSelected ? `${C.cyan}> ` : ' ';
84
- const currentTag = item.isCurrent ? ` ${C.yellow}<current>${C.reset}` : '';
85
-
86
- if (isSelected) {
87
- lines.push(`${pointer}${C.bold}${item.label}${C.reset}${currentTag}`);
88
- } else {
89
- lines.push(`${pointer}${C.dim}${item.label}${C.reset}${currentTag}`);
90
- }
91
- }
92
-
93
- if (end < items.length) {
94
- lines.push(` ${C.dim}\u2193 more${C.reset}`);
95
- }
96
-
97
- const output = lines.join('\n');
98
- process.stdout.write(output + '\n');
99
- renderedLines = lines.length;
100
- }
101
-
102
- // Pause readline, take over stdin
103
- rl.pause();
104
- const wasRaw = process.stdin.isRaw;
105
- if (process.stdin.isTTY) {
106
- process.stdin.setRawMode(true);
107
- }
108
- process.stdin.resume();
109
-
110
- function cleanup() {
111
- process.stdin.removeListener('keypress', onKeypress);
112
- if (process.stdin.isTTY && wasRaw !== undefined) {
113
- process.stdin.setRawMode(wasRaw);
114
- }
115
- rl.resume();
116
- }
117
-
118
- function onKeypress(str, key) {
119
- if (!key) return;
120
-
121
- if (key.name === 'up' || (key.ctrl && key.name === 'p')) {
122
- if (cursor > 0) {
123
- cursor--;
124
- render();
125
- }
126
- return;
127
- }
128
-
129
- if (key.name === 'down' || (key.ctrl && key.name === 'n')) {
130
- if (cursor < selectableIndices.length - 1) {
131
- cursor++;
132
- render();
133
- }
134
- return;
135
- }
136
-
137
- if (key.name === 'return') {
138
- const selectedItem = items[selectableIndices[cursor]];
139
- cleanup();
140
- resolve(selectedItem ? selectedItem.value : null);
141
- return;
142
- }
143
-
144
- if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
145
- cleanup();
146
- resolve(null);
147
- return;
148
- }
149
- }
150
-
151
- process.stdin.on('keypress', onKeypress);
152
- render();
153
- });
154
- }
155
-
156
- /**
157
- * Show the model picker and handle selection.
158
- * @param {readline.Interface} rl
159
- * @returns {Promise<boolean>} true if model was selected
160
- */
161
- async function showModelPicker(rl) {
162
- const providerList = listProviders();
163
- const activeProvider = getActiveProviderName();
164
- const activeModel = getActiveModelId();
165
-
166
- // Build picker items: provider headers + model entries
167
- const items = [];
168
- for (const p of providerList) {
169
- if (p.models.length === 0) continue;
170
-
171
- items.push({
172
- label: p.provider,
173
- value: null,
174
- isHeader: true,
175
- });
176
-
177
- for (const m of p.models) {
178
- const isCurrent = p.provider === activeProvider && m.id === activeModel;
179
- items.push({
180
- label: ` ${m.name} (${p.provider}:${m.id})`,
181
- value: `${p.provider}:${m.id}`,
182
- isCurrent,
183
- });
184
- }
185
- }
186
-
187
- const selected = await pickFromList(rl, items, {
188
- title: 'Select Model',
189
- });
190
-
191
- if (selected) {
192
- setActiveModel(selected);
193
- console.log(`${C.green}Switched to ${selected}${C.reset}`);
194
- return true;
195
- }
196
-
197
- console.log(`${C.dim}Cancelled${C.reset}`);
198
- return false;
199
- }
200
-
201
- module.exports = { pickFromList, showModelPicker };
package/cli/planner.js DELETED
@@ -1,282 +0,0 @@
1
- /**
2
- * cli/planner.js — Plan Mode
3
- * Structured planning workflow: analyze → plan → approve → execute
4
- */
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const readline = require('readline');
9
- const { C } = require('./ui');
10
-
11
- // Plan state
12
- let activePlan = null;
13
- let planMode = false;
14
-
15
- function getPlanDir() {
16
- return path.join(process.cwd(), '.nex', 'plans');
17
- }
18
-
19
- function ensureDir() {
20
- const dir = getPlanDir();
21
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
22
- }
23
-
24
- /**
25
- * @typedef {object} PlanStep
26
- * @property {string} description
27
- * @property {string[]} files — affected files
28
- * @property {'pending'|'in_progress'|'done'|'skipped'} status
29
- */
30
-
31
- /**
32
- * @typedef {object} Plan
33
- * @property {string} name
34
- * @property {string} task — original task description
35
- * @property {PlanStep[]} steps
36
- * @property {'draft'|'approved'|'executing'|'completed'} status
37
- * @property {string} createdAt
38
- * @property {string} [updatedAt]
39
- */
40
-
41
- /**
42
- * Create a new plan
43
- * @param {string} task
44
- * @param {PlanStep[]} steps
45
- * @returns {Plan}
46
- */
47
- function createPlan(task, steps = []) {
48
- activePlan = {
49
- name: `plan-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
50
- task,
51
- steps: steps.map((s) => ({
52
- description: s.description || s,
53
- files: s.files || [],
54
- status: 'pending',
55
- })),
56
- status: 'draft',
57
- createdAt: new Date().toISOString(),
58
- };
59
- return activePlan;
60
- }
61
-
62
- /**
63
- * Get the active plan
64
- * @returns {Plan|null}
65
- */
66
- function getActivePlan() {
67
- return activePlan;
68
- }
69
-
70
- /**
71
- * Set plan mode on/off
72
- */
73
- function setPlanMode(enabled) {
74
- planMode = enabled;
75
- }
76
-
77
- /**
78
- * Check if plan mode is active
79
- */
80
- function isPlanMode() {
81
- return planMode;
82
- }
83
-
84
- /**
85
- * Approve the current plan
86
- * @returns {boolean}
87
- */
88
- function approvePlan() {
89
- if (!activePlan || activePlan.status !== 'draft') return false;
90
- activePlan.status = 'approved';
91
- activePlan.updatedAt = new Date().toISOString();
92
- return true;
93
- }
94
-
95
- /**
96
- * Start executing the plan
97
- */
98
- function startExecution() {
99
- if (!activePlan || activePlan.status !== 'approved') return false;
100
- activePlan.status = 'executing';
101
- return true;
102
- }
103
-
104
- /**
105
- * Update step status
106
- * @param {number} index
107
- * @param {'pending'|'in_progress'|'done'|'skipped'} status
108
- */
109
- function updateStep(index, status) {
110
- if (!activePlan || index < 0 || index >= activePlan.steps.length) return false;
111
- activePlan.steps[index].status = status;
112
- activePlan.updatedAt = new Date().toISOString();
113
-
114
- // Check if all steps are done
115
- if (activePlan.steps.every((s) => s.status === 'done' || s.status === 'skipped')) {
116
- activePlan.status = 'completed';
117
- }
118
- return true;
119
- }
120
-
121
- /**
122
- * Format plan for display
123
- * @param {Plan} plan
124
- * @returns {string}
125
- */
126
- function formatPlan(plan) {
127
- if (!plan) return `${C.dim}No active plan${C.reset}`;
128
-
129
- const statusIcon = {
130
- draft: `${C.yellow}DRAFT${C.reset}`,
131
- approved: `${C.green}APPROVED${C.reset}`,
132
- executing: `${C.blue}EXECUTING${C.reset}`,
133
- completed: `${C.green}COMPLETED${C.reset}`,
134
- };
135
-
136
- const lines = [];
137
- lines.push(`\n${C.bold}${C.cyan}Plan: ${plan.task}${C.reset}`);
138
- lines.push(`${C.dim}Status: ${statusIcon[plan.status] || plan.status}${C.reset}\n`);
139
-
140
- for (let i = 0; i < plan.steps.length; i++) {
141
- const step = plan.steps[i];
142
- let icon;
143
- switch (step.status) {
144
- case 'done': icon = `${C.green}✓${C.reset}`; break;
145
- case 'in_progress': icon = `${C.blue}→${C.reset}`; break;
146
- case 'skipped': icon = `${C.dim}○${C.reset}`; break;
147
- default: icon = `${C.dim} ${C.reset}`;
148
- }
149
- lines.push(` ${icon} ${C.bold}Step ${i + 1}:${C.reset} ${step.description}`);
150
- if (step.files.length > 0) {
151
- lines.push(` ${C.dim}Files: ${step.files.join(', ')}${C.reset}`);
152
- }
153
- }
154
- lines.push('');
155
- return lines.join('\n');
156
- }
157
-
158
- /**
159
- * Save plan to .nex/plans/
160
- */
161
- function savePlan(plan) {
162
- if (!plan) plan = activePlan;
163
- if (!plan) return null;
164
- ensureDir();
165
- const filePath = path.join(getPlanDir(), `${plan.name}.json`);
166
- fs.writeFileSync(filePath, JSON.stringify(plan, null, 2), 'utf-8');
167
- return filePath;
168
- }
169
-
170
- /**
171
- * Load a plan from disk
172
- */
173
- function loadPlan(name) {
174
- const filePath = path.join(getPlanDir(), `${name}.json`);
175
- if (!fs.existsSync(filePath)) return null;
176
- try {
177
- const plan = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
178
- activePlan = plan;
179
- return plan;
180
- } catch {
181
- return null;
182
- }
183
- }
184
-
185
- /**
186
- * List all saved plans
187
- */
188
- function listPlans() {
189
- ensureDir();
190
- const dir = getPlanDir();
191
- const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
192
- const plans = [];
193
- for (const f of files) {
194
- try {
195
- const data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
196
- plans.push({
197
- name: data.name,
198
- task: data.task,
199
- status: data.status,
200
- steps: data.steps ? data.steps.length : 0,
201
- createdAt: data.createdAt,
202
- });
203
- } catch {
204
- // skip corrupt
205
- }
206
- }
207
- return plans.sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''));
208
- }
209
-
210
- /**
211
- * Clear the active plan
212
- */
213
- function clearPlan() {
214
- activePlan = null;
215
- planMode = false;
216
- }
217
-
218
- /**
219
- * Get plan-mode system prompt addition
220
- * Instructs the LLM to only analyze and plan, not execute
221
- */
222
- function getPlanModePrompt() {
223
- return `
224
- PLAN MODE ACTIVE: You are in analysis-only mode.
225
-
226
- # Restrictions
227
- - Use ONLY read operations: read_file, list_directory, search_files, glob, grep
228
- - DO NOT modify any files (no write_file, edit_file, patch_file, bash with write ops)
229
-
230
- # Analysis Phase
231
- Investigate before planning:
232
- - Scope: What files and modules are affected?
233
- - Architecture: How does the current code work? What patterns does it follow?
234
- - Dependencies: What depends on the code being changed? What might break?
235
- - Tests: What test coverage exists? What new tests are needed?
236
-
237
- # Plan Output Format
238
- For each step, provide:
239
- - **What**: Clear description of the change
240
- - **Where**: Specific files and line ranges
241
- - **How**: Implementation approach (edit, create, delete)
242
- - **Risk**: What could go wrong and how to mitigate
243
-
244
- # Rules
245
- - Order steps by dependency — later steps can depend on earlier ones, not vice versa.
246
- - Flag steps that need tests and specify what to test.
247
- - List any assumptions you're making about the codebase.
248
- - Present the plan to the user and wait for explicit "approve" before executing.`;
249
- }
250
-
251
- // Autonomy levels
252
- const AUTONOMY_LEVELS = ['interactive', 'semi-auto', 'autonomous'];
253
- let autonomyLevel = 'interactive';
254
-
255
- function setAutonomyLevel(level) {
256
- if (!AUTONOMY_LEVELS.includes(level)) return false;
257
- autonomyLevel = level;
258
- return true;
259
- }
260
-
261
- function getAutonomyLevel() {
262
- return autonomyLevel;
263
- }
264
-
265
- module.exports = {
266
- createPlan,
267
- getActivePlan,
268
- setPlanMode,
269
- isPlanMode,
270
- approvePlan,
271
- startExecution,
272
- updateStep,
273
- formatPlan,
274
- savePlan,
275
- loadPlan,
276
- listPlans,
277
- clearPlan,
278
- getPlanModePrompt,
279
- setAutonomyLevel,
280
- getAutonomyLevel,
281
- AUTONOMY_LEVELS,
282
- };