neuro-cli 4.1.0

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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,771 @@
1
+ // ============================================================
2
+ // NeuroCLI - Vim Keybindings Mode
3
+ // Vim-like input mode (normal/insert/visual) with hjkl
4
+ // navigation, dd, yy, p, command mode (:), mode indicator
5
+ // Integration with readline
6
+ // ============================================================
7
+ import chalk from 'chalk';
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
11
+ // -----------------------------------------------------------
12
+ // Default config
13
+ // -----------------------------------------------------------
14
+ const VIM_CONFIG_PATH = join(homedir(), '.neuro', 'vim-config.json');
15
+ function defaultConfig() {
16
+ return {
17
+ enabled: false,
18
+ showModeIndicator: true,
19
+ keyMappings: {},
20
+ bellOnError: true,
21
+ };
22
+ }
23
+ // -----------------------------------------------------------
24
+ // VimModeManager
25
+ // -----------------------------------------------------------
26
+ export class VimModeManager {
27
+ config;
28
+ mode = 'insert'; // Start in insert mode (user-friendly)
29
+ registers = new Map();
30
+ commandBuffer = '';
31
+ normalBuffer = '';
32
+ visualStart = 0;
33
+ cursorPosition = 0;
34
+ lineBuffer = '';
35
+ lastAction = '';
36
+ repeatCount = 0;
37
+ commandHistory = [];
38
+ commandHistoryIndex = -1;
39
+ rl = null;
40
+ keyHandlerAttached = false;
41
+ constructor(config) {
42
+ this.config = { ...defaultConfig(), ...config };
43
+ this.loadConfig();
44
+ if (this.config.enabled) {
45
+ this.mode = 'insert'; // Start in insert for better UX
46
+ }
47
+ }
48
+ // ----------------------------------------------------------
49
+ // Public API
50
+ // ----------------------------------------------------------
51
+ /**
52
+ * Check if vim mode is enabled
53
+ */
54
+ isEnabled() {
55
+ return this.config.enabled;
56
+ }
57
+ /**
58
+ * Enable vim mode
59
+ */
60
+ enable() {
61
+ this.config.enabled = true;
62
+ this.mode = 'insert';
63
+ this.saveConfig();
64
+ console.log(chalk.green('Vim mode enabled. Press Esc to enter normal mode.'));
65
+ }
66
+ /**
67
+ * Disable vim mode
68
+ */
69
+ disable() {
70
+ this.config.enabled = false;
71
+ this.mode = 'insert';
72
+ this.saveConfig();
73
+ console.log(chalk.gray('Vim mode disabled. Standard readline bindings active.'));
74
+ }
75
+ /**
76
+ * Toggle vim mode
77
+ */
78
+ toggle() {
79
+ if (this.config.enabled)
80
+ this.disable();
81
+ else
82
+ this.enable();
83
+ return this.config.enabled;
84
+ }
85
+ /**
86
+ * Get current mode
87
+ */
88
+ getMode() {
89
+ return this.mode;
90
+ }
91
+ /**
92
+ * Set current mode
93
+ */
94
+ setMode(mode) {
95
+ this.mode = mode;
96
+ this.commandBuffer = '';
97
+ this.normalBuffer = '';
98
+ this.repeatCount = 0;
99
+ }
100
+ /**
101
+ * Get mode indicator string for the prompt
102
+ */
103
+ getModeIndicator() {
104
+ if (!this.config.enabled || !this.config.showModeIndicator)
105
+ return '';
106
+ switch (this.mode) {
107
+ case 'normal':
108
+ return chalk.bgBlue.white.bold(' NORMAL ');
109
+ case 'insert':
110
+ return chalk.bgGreen.white.bold(' INSERT ');
111
+ case 'visual':
112
+ return chalk.bgMagenta.white.bold(' VISUAL ');
113
+ case 'command':
114
+ return chalk.bgYellow.black.bold(' CMD ');
115
+ default:
116
+ return '';
117
+ }
118
+ }
119
+ /**
120
+ * Get the modified prompt with mode indicator
121
+ */
122
+ getModifiedPrompt(basePrompt) {
123
+ if (!this.config.enabled)
124
+ return basePrompt;
125
+ const indicator = this.getModeIndicator();
126
+ return indicator ? `${indicator} ${basePrompt}` : basePrompt;
127
+ }
128
+ /**
129
+ * Attach to a readline interface for key handling
130
+ */
131
+ attachToReadline(rl) {
132
+ if (!this.config.enabled)
133
+ return;
134
+ this.rl = rl;
135
+ if (!this.keyHandlerAttached && typeof process.stdin.setRawMode === 'function') {
136
+ // We handle vim keys at a higher level via processKey
137
+ this.keyHandlerAttached = true;
138
+ }
139
+ }
140
+ /**
141
+ * Detach from readline interface
142
+ */
143
+ detachFromReadline() {
144
+ this.rl = null;
145
+ this.keyHandlerAttached = false;
146
+ }
147
+ /**
148
+ * Process a key press and return the action to take
149
+ */
150
+ processKey(key) {
151
+ if (!this.config.enabled)
152
+ return null;
153
+ // Handle escape first
154
+ if (key === '\x1b' || key === 'Escape') {
155
+ return this.handleEscape();
156
+ }
157
+ switch (this.mode) {
158
+ case 'normal':
159
+ return this.handleNormalKey(key);
160
+ case 'insert':
161
+ return this.handleInsertKey(key);
162
+ case 'visual':
163
+ return this.handleVisualKey(key);
164
+ case 'command':
165
+ return this.handleCommandKey(key);
166
+ default:
167
+ return null;
168
+ }
169
+ }
170
+ /**
171
+ * Get all yanked text from a register
172
+ */
173
+ getRegister(name) {
174
+ const reg = this.registers.get(name);
175
+ return reg ? reg.content : '';
176
+ }
177
+ /**
178
+ * Set register content
179
+ */
180
+ setRegister(name, content, type = 'line') {
181
+ this.registers.set(name, { name, content, type });
182
+ }
183
+ /**
184
+ * Get current line buffer
185
+ */
186
+ getLineBuffer() {
187
+ return this.lineBuffer;
188
+ }
189
+ /**
190
+ * Set current line buffer
191
+ */
192
+ setLineBuffer(buffer) {
193
+ this.lineBuffer = buffer;
194
+ }
195
+ /**
196
+ * Get cursor position
197
+ */
198
+ getCursorPosition() {
199
+ return this.cursorPosition;
200
+ }
201
+ /**
202
+ * Set cursor position
203
+ */
204
+ setCursorPosition(pos) {
205
+ this.cursorPosition = Math.max(0, Math.min(pos, this.lineBuffer.length));
206
+ }
207
+ /**
208
+ * Execute a vim command (e.g. :w, :q, :set)
209
+ */
210
+ executeCommand(cmd) {
211
+ const trimmed = cmd.trim();
212
+ // Built-in commands
213
+ const commands = [
214
+ {
215
+ name: 'set',
216
+ pattern: /^set\s+/,
217
+ handler: (args) => this.handleSetCommand(args),
218
+ description: 'Set vim options',
219
+ },
220
+ {
221
+ name: 'w',
222
+ pattern: /^w$/,
223
+ handler: () => console.log(chalk.gray('Session auto-saved.')),
224
+ description: 'Write/save (auto-saved in NeuroCLI)',
225
+ },
226
+ {
227
+ name: 'q',
228
+ pattern: /^q(!)?$/,
229
+ handler: (args) => {
230
+ if (args.includes('!')) {
231
+ process.exit(0);
232
+ }
233
+ else {
234
+ console.log(chalk.yellow('Use :q! to force quit.'));
235
+ }
236
+ },
237
+ description: 'Quit',
238
+ },
239
+ {
240
+ name: 'help',
241
+ pattern: /^help$/,
242
+ handler: () => this.printHelp(),
243
+ description: 'Show vim mode help',
244
+ },
245
+ {
246
+ name: 'registers',
247
+ pattern: /^registers$/,
248
+ handler: () => this.printRegisters(),
249
+ description: 'Show register contents',
250
+ },
251
+ {
252
+ name: 'mode',
253
+ pattern: /^mode$/,
254
+ handler: () => console.log(`Current mode: ${chalk.cyan(this.mode)}`),
255
+ description: 'Show current mode',
256
+ },
257
+ {
258
+ name: 'history',
259
+ pattern: /^history$/,
260
+ handler: () => this.printCommandHistory(),
261
+ description: 'Show command history',
262
+ },
263
+ ];
264
+ for (const command of commands) {
265
+ if (command.pattern.test(trimmed)) {
266
+ const args = trimmed.replace(command.pattern, '');
267
+ command.handler(args, this);
268
+ this.commandHistory.push(trimmed);
269
+ return;
270
+ }
271
+ }
272
+ console.log(chalk.yellow(`Unknown command: :${trimmed}. Type :help for vim mode help.`));
273
+ this.commandHistory.push(trimmed);
274
+ }
275
+ /**
276
+ * Print vim mode help
277
+ */
278
+ printHelp() {
279
+ console.log('');
280
+ console.log(chalk.bold('--- NeuroCLI Vim Mode Help ---'));
281
+ console.log('');
282
+ console.log(chalk.cyan(' Mode Switching:'));
283
+ console.log(' Esc - Switch to Normal mode');
284
+ console.log(' i - Enter Insert mode');
285
+ console.log(' a - Enter Insert mode (after cursor)');
286
+ console.log(' v - Enter Visual mode');
287
+ console.log(' : - Enter Command mode');
288
+ console.log('');
289
+ console.log(chalk.cyan(' Normal Mode Navigation:'));
290
+ console.log(' h - Move cursor left');
291
+ console.log(' j - Next history entry');
292
+ console.log(' k - Previous history entry');
293
+ console.log(' l - Move cursor right');
294
+ console.log(' 0 - Move to start of line');
295
+ console.log(' $ - Move to end of line');
296
+ console.log(' w - Move to next word');
297
+ console.log(' b - Move to previous word');
298
+ console.log('');
299
+ console.log(chalk.cyan(' Normal Mode Editing:'));
300
+ console.log(' dd - Delete entire line');
301
+ console.log(' dw - Delete word');
302
+ console.log(' x - Delete character under cursor');
303
+ console.log(' yy - Yank (copy) entire line');
304
+ console.log(' yw - Yank word');
305
+ console.log(' p - Paste after cursor');
306
+ console.log(' P - Paste before cursor');
307
+ console.log(' u - Undo');
308
+ console.log(' Ctrl+r - Redo');
309
+ console.log('');
310
+ console.log(chalk.cyan(' Command Mode:'));
311
+ console.log(' :help - Show this help');
312
+ console.log(' :w - Save session');
313
+ console.log(' :q! - Force quit');
314
+ console.log(' :set <opt> - Set option');
315
+ console.log(' :registers - Show registers');
316
+ console.log(' :mode - Show current mode');
317
+ console.log('');
318
+ console.log(chalk.bold('-------------------------------'));
319
+ console.log('');
320
+ }
321
+ /**
322
+ * Print register contents
323
+ */
324
+ printRegisters() {
325
+ console.log('');
326
+ console.log(chalk.bold('--- Vim Registers ---'));
327
+ for (const [name, reg] of this.registers) {
328
+ const preview = reg.content.length > 40 ? reg.content.slice(0, 40) + '...' : reg.content;
329
+ console.log(` "${name}: ${chalk.cyan(preview)} (${reg.type})`);
330
+ }
331
+ if (this.registers.size === 0) {
332
+ console.log(chalk.gray(' (no registers set)'));
333
+ }
334
+ console.log('');
335
+ }
336
+ /**
337
+ * Get config
338
+ */
339
+ getConfig() {
340
+ return { ...this.config };
341
+ }
342
+ // ----------------------------------------------------------
343
+ // Private key handlers
344
+ // ----------------------------------------------------------
345
+ handleEscape() {
346
+ if (this.mode === 'insert' || this.mode === 'visual') {
347
+ this.mode = 'normal';
348
+ this.normalBuffer = '';
349
+ this.repeatCount = 0;
350
+ return { mode: 'normal', key: '\x1b', action: 'switch_mode', description: 'Switch to normal mode' };
351
+ }
352
+ if (this.mode === 'command') {
353
+ this.mode = 'normal';
354
+ this.commandBuffer = '';
355
+ return { mode: 'normal', key: '\x1b', action: 'switch_mode', description: 'Cancel command mode' };
356
+ }
357
+ return { mode: 'normal', key: '\x1b', action: 'no_op', description: 'Already in normal mode' };
358
+ }
359
+ handleNormalKey(key) {
360
+ // Accumulate repeat count
361
+ if (/^[1-9]$/.test(key) && this.normalBuffer === '') {
362
+ this.repeatCount = this.repeatCount * 10 + parseInt(key);
363
+ return { mode: 'normal', key, action: 'repeat_count', description: `Repeat count: ${this.repeatCount}` };
364
+ }
365
+ this.normalBuffer += key;
366
+ // Check for two-character commands
367
+ if (this.normalBuffer.length === 1) {
368
+ // Single-character commands
369
+ switch (key) {
370
+ case 'i':
371
+ this.mode = 'insert';
372
+ this.normalBuffer = '';
373
+ return { mode: 'insert', key, action: 'switch_mode', description: 'Enter insert mode' };
374
+ case 'a':
375
+ this.mode = 'insert';
376
+ this.cursorPosition++;
377
+ this.normalBuffer = '';
378
+ return { mode: 'insert', key, action: 'append', description: 'Append (insert after cursor)' };
379
+ case 'A':
380
+ this.mode = 'insert';
381
+ this.cursorPosition = this.lineBuffer.length;
382
+ this.normalBuffer = '';
383
+ return { mode: 'insert', key, action: 'append_end', description: 'Append at end of line' };
384
+ case 'I':
385
+ this.mode = 'insert';
386
+ this.cursorPosition = 0;
387
+ this.normalBuffer = '';
388
+ return { mode: 'insert', key, action: 'insert_start', description: 'Insert at start of line' };
389
+ case 'v':
390
+ this.mode = 'visual';
391
+ this.visualStart = this.cursorPosition;
392
+ this.normalBuffer = '';
393
+ return { mode: 'visual', key, action: 'switch_mode', description: 'Enter visual mode' };
394
+ case ':':
395
+ this.mode = 'command';
396
+ this.commandBuffer = '';
397
+ this.normalBuffer = '';
398
+ return { mode: 'command', key, action: 'switch_mode', description: 'Enter command mode' };
399
+ case 'h':
400
+ this.cursorPosition = Math.max(0, this.cursorPosition - 1);
401
+ this.normalBuffer = '';
402
+ return { mode: 'normal', key, action: 'cursor_left', description: 'Move cursor left' };
403
+ case 'l':
404
+ this.cursorPosition = Math.min(this.lineBuffer.length, this.cursorPosition + 1);
405
+ this.normalBuffer = '';
406
+ return { mode: 'normal', key, action: 'cursor_right', description: 'Move cursor right' };
407
+ case 'j':
408
+ this.normalBuffer = '';
409
+ return { mode: 'normal', key, action: 'history_next', description: 'Next history entry' };
410
+ case 'k':
411
+ this.normalBuffer = '';
412
+ return { mode: 'normal', key, action: 'history_prev', description: 'Previous history entry' };
413
+ case '0':
414
+ if (this.repeatCount === 0) {
415
+ this.cursorPosition = 0;
416
+ this.normalBuffer = '';
417
+ return { mode: 'normal', key, action: 'cursor_start', description: 'Move to start of line' };
418
+ }
419
+ break;
420
+ case '$':
421
+ this.cursorPosition = this.lineBuffer.length;
422
+ this.normalBuffer = '';
423
+ return { mode: 'normal', key, action: 'cursor_end', description: 'Move to end of line' };
424
+ case 'w':
425
+ this.moveWordForward();
426
+ this.normalBuffer = '';
427
+ return { mode: 'normal', key, action: 'word_forward', description: 'Move to next word' };
428
+ case 'b':
429
+ this.moveWordBackward();
430
+ this.normalBuffer = '';
431
+ return { mode: 'normal', key, action: 'word_backward', description: 'Move to previous word' };
432
+ case 'x':
433
+ this.deleteCharAtCursor();
434
+ this.normalBuffer = '';
435
+ return { mode: 'normal', key, action: 'delete_char', description: 'Delete character under cursor' };
436
+ case 'p':
437
+ this.pasteAfter();
438
+ this.normalBuffer = '';
439
+ return { mode: 'normal', key, action: 'paste_after', description: 'Paste after cursor' };
440
+ case 'P':
441
+ this.pasteBefore();
442
+ this.normalBuffer = '';
443
+ return { mode: 'normal', key, action: 'paste_before', description: 'Paste before cursor' };
444
+ case 'u':
445
+ this.normalBuffer = '';
446
+ return { mode: 'normal', key, action: 'undo', description: 'Undo' };
447
+ case 'r':
448
+ // Waiting for next key (replace char)
449
+ return { mode: 'normal', key, action: 'waiting', description: 'Waiting for replace target' };
450
+ case 'o':
451
+ this.mode = 'insert';
452
+ this.lineBuffer = '';
453
+ this.cursorPosition = 0;
454
+ this.normalBuffer = '';
455
+ return { mode: 'insert', key, action: 'open_line_below', description: 'Open new line below' };
456
+ case 'O':
457
+ this.mode = 'insert';
458
+ this.lineBuffer = '';
459
+ this.cursorPosition = 0;
460
+ this.normalBuffer = '';
461
+ return { mode: 'insert', key, action: 'open_line_above', description: 'Open new line above' };
462
+ }
463
+ }
464
+ // Two-character commands
465
+ if (this.normalBuffer.length === 2) {
466
+ const cmd = this.normalBuffer;
467
+ this.normalBuffer = '';
468
+ switch (cmd) {
469
+ case 'dd':
470
+ this.setRegister('', this.lineBuffer, 'line');
471
+ this.lineBuffer = '';
472
+ this.cursorPosition = 0;
473
+ this.repeatCount = 0;
474
+ return { mode: 'normal', key: cmd, action: 'delete_line', description: 'Delete entire line' };
475
+ case 'yy':
476
+ this.setRegister('', this.lineBuffer, 'line');
477
+ this.repeatCount = 0;
478
+ return { mode: 'normal', key: cmd, action: 'yank_line', description: 'Yank (copy) line' };
479
+ case 'dw':
480
+ this.deleteWordForward();
481
+ this.repeatCount = 0;
482
+ return { mode: 'normal', key: cmd, action: 'delete_word', description: 'Delete word' };
483
+ case 'yw':
484
+ this.yankWordForward();
485
+ this.repeatCount = 0;
486
+ return { mode: 'normal', key: cmd, action: 'yank_word', description: 'Yank word' };
487
+ case 'cc':
488
+ this.setRegister('', this.lineBuffer, 'line');
489
+ this.lineBuffer = '';
490
+ this.cursorPosition = 0;
491
+ this.mode = 'insert';
492
+ this.repeatCount = 0;
493
+ return { mode: 'insert', key: cmd, action: 'change_line', description: 'Change entire line' };
494
+ case 'cw':
495
+ this.changeWordForward();
496
+ this.mode = 'insert';
497
+ this.repeatCount = 0;
498
+ return { mode: 'insert', key: cmd, action: 'change_word', description: 'Change word' };
499
+ default:
500
+ // Handle replace char (r followed by char)
501
+ if (cmd[0] === 'r' && cmd.length === 2) {
502
+ this.replaceCharAtCursor(cmd[1]);
503
+ this.repeatCount = 0;
504
+ return { mode: 'normal', key: cmd, action: 'replace_char', description: `Replace char with '${cmd[1]}'` };
505
+ }
506
+ if (this.config.bellOnError) {
507
+ return { mode: 'normal', key: cmd, action: 'bell', description: `Unknown command: ${cmd}` };
508
+ }
509
+ return { mode: 'normal', key: cmd, action: 'no_op', description: `Unknown command: ${cmd}` };
510
+ }
511
+ }
512
+ return { mode: 'normal', key, action: 'waiting', description: 'Waiting for next key' };
513
+ }
514
+ handleInsertKey(key) {
515
+ // In insert mode, most keys are passed through to readline
516
+ // We only intercept special keys
517
+ if (key === '\x1b' || key === 'Escape') {
518
+ return this.handleEscape();
519
+ }
520
+ // Ctrl+C in insert mode goes to normal mode
521
+ if (key === '\x03') {
522
+ this.mode = 'normal';
523
+ return { mode: 'normal', key, action: 'switch_mode', description: 'Ctrl+C: switch to normal mode' };
524
+ }
525
+ // Pass through to readline
526
+ return { mode: 'insert', key, action: 'passthrough', description: 'Insert mode passthrough' };
527
+ }
528
+ handleVisualKey(key) {
529
+ switch (key) {
530
+ case '\x1b':
531
+ case 'Escape':
532
+ this.mode = 'normal';
533
+ return { mode: 'normal', key, action: 'switch_mode', description: 'Exit visual mode' };
534
+ case 'h':
535
+ this.cursorPosition = Math.max(0, this.cursorPosition - 1);
536
+ return { mode: 'visual', key, action: 'cursor_left', description: 'Move selection left' };
537
+ case 'l':
538
+ this.cursorPosition = Math.min(this.lineBuffer.length, this.cursorPosition + 1);
539
+ return { mode: 'visual', key, action: 'cursor_right', description: 'Move selection right' };
540
+ case 'y':
541
+ this.yankVisualSelection();
542
+ this.mode = 'normal';
543
+ return { mode: 'normal', key, action: 'yank_selection', description: 'Yank visual selection' };
544
+ case 'd':
545
+ case 'x':
546
+ this.deleteVisualSelection();
547
+ this.mode = 'normal';
548
+ return { mode: 'normal', key, action: 'delete_selection', description: 'Delete visual selection' };
549
+ default:
550
+ return { mode: 'visual', key, action: 'no_op', description: `Unknown visual key: ${key}` };
551
+ }
552
+ }
553
+ handleCommandKey(key) {
554
+ // Enter executes command
555
+ if (key === '\r' || key === '\n') {
556
+ const cmd = this.commandBuffer;
557
+ this.commandBuffer = '';
558
+ this.mode = 'normal';
559
+ this.executeCommand(cmd);
560
+ return { mode: 'normal', key, action: 'execute_command', description: `Execute: :${cmd}` };
561
+ }
562
+ // Backspace
563
+ if (key === '\x7f' || key === 'Backspace') {
564
+ this.commandBuffer = this.commandBuffer.slice(0, -1);
565
+ if (this.commandBuffer === '') {
566
+ this.mode = 'normal';
567
+ return { mode: 'normal', key, action: 'switch_mode', description: 'Cancel command' };
568
+ }
569
+ return { mode: 'command', key, action: 'backspace', description: 'Command backspace' };
570
+ }
571
+ // Escape cancels
572
+ if (key === '\x1b' || key === 'Escape') {
573
+ this.commandBuffer = '';
574
+ this.mode = 'normal';
575
+ return { mode: 'normal', key, action: 'switch_mode', description: 'Cancel command' };
576
+ }
577
+ // Up/Down for command history
578
+ if (key === '\x1b[A' || key === 'ArrowUp') {
579
+ if (this.commandHistory.length > 0) {
580
+ this.commandHistoryIndex = Math.min(this.commandHistoryIndex + 1, this.commandHistory.length - 1);
581
+ this.commandBuffer = this.commandHistory[this.commandHistory.length - 1 - this.commandHistoryIndex];
582
+ }
583
+ return { mode: 'command', key, action: 'history_prev', description: 'Previous command' };
584
+ }
585
+ if (key === '\x1b[B' || key === 'ArrowDown') {
586
+ this.commandHistoryIndex = Math.max(this.commandHistoryIndex - 1, -1);
587
+ if (this.commandHistoryIndex === -1) {
588
+ this.commandBuffer = '';
589
+ }
590
+ else {
591
+ this.commandBuffer = this.commandHistory[this.commandHistory.length - 1 - this.commandHistoryIndex];
592
+ }
593
+ return { mode: 'command', key, action: 'history_next', description: 'Next command' };
594
+ }
595
+ // Regular character
596
+ if (key.length === 1 && key.charCodeAt(0) >= 32) {
597
+ this.commandBuffer += key;
598
+ return { mode: 'command', key, action: 'type', description: `Command: :${this.commandBuffer}` };
599
+ }
600
+ return { mode: 'command', key, action: 'no_op', description: 'Ignored key in command mode' };
601
+ }
602
+ // ----------------------------------------------------------
603
+ // Private text manipulation helpers
604
+ // ----------------------------------------------------------
605
+ moveWordForward() {
606
+ const text = this.lineBuffer;
607
+ let pos = this.cursorPosition;
608
+ // Skip current word
609
+ while (pos < text.length && /\w/.test(text[pos]))
610
+ pos++;
611
+ // Skip whitespace
612
+ while (pos < text.length && /\s/.test(text[pos]))
613
+ pos++;
614
+ this.cursorPosition = pos;
615
+ }
616
+ moveWordBackward() {
617
+ const text = this.lineBuffer;
618
+ let pos = this.cursorPosition - 1;
619
+ // Skip whitespace
620
+ while (pos > 0 && /\s/.test(text[pos]))
621
+ pos--;
622
+ // Skip current word
623
+ while (pos > 0 && /\w/.test(text[pos - 1]))
624
+ pos--;
625
+ this.cursorPosition = Math.max(0, pos);
626
+ }
627
+ deleteCharAtCursor() {
628
+ if (this.cursorPosition < this.lineBuffer.length) {
629
+ const deleted = this.lineBuffer[this.cursorPosition];
630
+ this.lineBuffer = this.lineBuffer.slice(0, this.cursorPosition) + this.lineBuffer.slice(this.cursorPosition + 1);
631
+ this.setRegister('', deleted, 'char');
632
+ }
633
+ }
634
+ replaceCharAtCursor(char) {
635
+ if (this.cursorPosition < this.lineBuffer.length) {
636
+ this.lineBuffer = this.lineBuffer.slice(0, this.cursorPosition) + char + this.lineBuffer.slice(this.cursorPosition + 1);
637
+ }
638
+ }
639
+ deleteWordForward() {
640
+ const start = this.cursorPosition;
641
+ const text = this.lineBuffer;
642
+ let end = start;
643
+ while (end < text.length && /\w/.test(text[end]))
644
+ end++;
645
+ while (end < text.length && /\s/.test(text[end]))
646
+ end++;
647
+ const deleted = text.slice(start, end);
648
+ this.setRegister('', deleted, 'char');
649
+ this.lineBuffer = text.slice(0, start) + text.slice(end);
650
+ }
651
+ yankWordForward() {
652
+ const start = this.cursorPosition;
653
+ const text = this.lineBuffer;
654
+ let end = start;
655
+ while (end < text.length && /\w/.test(text[end]))
656
+ end++;
657
+ const yanked = text.slice(start, end);
658
+ this.setRegister('', yanked, 'char');
659
+ }
660
+ changeWordForward() {
661
+ const start = this.cursorPosition;
662
+ const text = this.lineBuffer;
663
+ let end = start;
664
+ while (end < text.length && /\w/.test(text[end]))
665
+ end++;
666
+ const deleted = text.slice(start, end);
667
+ this.setRegister('', deleted, 'char');
668
+ this.lineBuffer = text.slice(0, start) + text.slice(end);
669
+ this.cursorPosition = start;
670
+ }
671
+ pasteAfter() {
672
+ const reg = this.registers.get('');
673
+ if (!reg)
674
+ return;
675
+ if (reg.type === 'line') {
676
+ this.lineBuffer += reg.content;
677
+ this.cursorPosition = this.lineBuffer.length;
678
+ }
679
+ else {
680
+ const pos = this.cursorPosition + 1;
681
+ this.lineBuffer = this.lineBuffer.slice(0, pos) + reg.content + this.lineBuffer.slice(pos);
682
+ this.cursorPosition = pos + reg.content.length;
683
+ }
684
+ }
685
+ pasteBefore() {
686
+ const reg = this.registers.get('');
687
+ if (!reg)
688
+ return;
689
+ if (reg.type === 'line') {
690
+ this.lineBuffer = reg.content + this.lineBuffer;
691
+ this.cursorPosition = 0;
692
+ }
693
+ else {
694
+ this.lineBuffer = this.lineBuffer.slice(0, this.cursorPosition) + reg.content + this.lineBuffer.slice(this.cursorPosition);
695
+ this.cursorPosition += reg.content.length;
696
+ }
697
+ }
698
+ yankVisualSelection() {
699
+ const start = Math.min(this.visualStart, this.cursorPosition);
700
+ const end = Math.max(this.visualStart, this.cursorPosition);
701
+ const yanked = this.lineBuffer.slice(start, end);
702
+ this.setRegister('', yanked, 'char');
703
+ }
704
+ deleteVisualSelection() {
705
+ const start = Math.min(this.visualStart, this.cursorPosition);
706
+ const end = Math.max(this.visualStart, this.cursorPosition);
707
+ const deleted = this.lineBuffer.slice(start, end);
708
+ this.setRegister('', deleted, 'char');
709
+ this.lineBuffer = this.lineBuffer.slice(0, start) + this.lineBuffer.slice(end);
710
+ this.cursorPosition = start;
711
+ }
712
+ handleSetCommand(args) {
713
+ const parts = args.trim().split(/\s+/);
714
+ if (parts.length === 0)
715
+ return;
716
+ switch (parts[0]) {
717
+ case 'number':
718
+ case 'nu':
719
+ this.config.showModeIndicator = true;
720
+ console.log(chalk.gray('Mode indicator enabled.'));
721
+ break;
722
+ case 'nonumber':
723
+ case 'nonu':
724
+ this.config.showModeIndicator = false;
725
+ console.log(chalk.gray('Mode indicator disabled.'));
726
+ break;
727
+ case 'bell':
728
+ this.config.bellOnError = true;
729
+ console.log(chalk.gray('Bell on error enabled.'));
730
+ break;
731
+ case 'nobell':
732
+ this.config.bellOnError = false;
733
+ console.log(chalk.gray('Bell on error disabled.'));
734
+ break;
735
+ default:
736
+ console.log(chalk.yellow(`Unknown option: ${parts[0]}`));
737
+ }
738
+ this.saveConfig();
739
+ }
740
+ printCommandHistory() {
741
+ console.log('');
742
+ console.log(chalk.bold('--- Command History ---'));
743
+ for (let i = this.commandHistory.length - 1; i >= Math.max(0, this.commandHistory.length - 20); i--) {
744
+ console.log(` :${this.commandHistory[i]}`);
745
+ }
746
+ if (this.commandHistory.length === 0) {
747
+ console.log(chalk.gray(' (no commands in history)'));
748
+ }
749
+ console.log('');
750
+ }
751
+ saveConfig() {
752
+ try {
753
+ const dir = join(VIM_CONFIG_PATH, '..');
754
+ if (!existsSync(dir))
755
+ mkdirSync(dir, { recursive: true });
756
+ writeFileSync(VIM_CONFIG_PATH, JSON.stringify(this.config, null, 2), 'utf-8');
757
+ }
758
+ catch { /* Silently fail */ }
759
+ }
760
+ loadConfig() {
761
+ try {
762
+ if (existsSync(VIM_CONFIG_PATH)) {
763
+ const raw = readFileSync(VIM_CONFIG_PATH, 'utf-8');
764
+ const saved = JSON.parse(raw);
765
+ this.config = { ...this.config, ...saved };
766
+ }
767
+ }
768
+ catch { /* Silently fail */ }
769
+ }
770
+ }
771
+ //# sourceMappingURL=vim-mode.js.map