osai-agent 4.0.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 (86) hide show
  1. package/LICENSE +7 -0
  2. package/package.json +72 -0
  3. package/src/agent/context.js +141 -0
  4. package/src/agent/loop/context-summary.js +196 -0
  5. package/src/agent/loop/directory-utils.js +102 -0
  6. package/src/agent/loop/local.js +196 -0
  7. package/src/agent/loop/loop-detection.js +288 -0
  8. package/src/agent/loop/stream-parser.js +515 -0
  9. package/src/agent/loop/tool-executor.js +470 -0
  10. package/src/agent/loop/verification.js +263 -0
  11. package/src/agent/loop/websocket.js +80 -0
  12. package/src/agent/prompt.js +259 -0
  13. package/src/agent/react-loop.js +697 -0
  14. package/src/agent/subagent.js +263 -0
  15. package/src/commands/config.js +53 -0
  16. package/src/commands/connect.js +190 -0
  17. package/src/commands/devices.js +121 -0
  18. package/src/commands/login.js +77 -0
  19. package/src/commands/logout.js +31 -0
  20. package/src/commands/mcp.js +258 -0
  21. package/src/commands/provider.js +633 -0
  22. package/src/commands/register.js +74 -0
  23. package/src/commands/run.js +150 -0
  24. package/src/commands/search.js +64 -0
  25. package/src/commands/session.js +57 -0
  26. package/src/commands/skills.js +54 -0
  27. package/src/commands/stop-subagent.js +58 -0
  28. package/src/index.js +208 -0
  29. package/src/llm/direct.js +317 -0
  30. package/src/memory/store.js +215 -0
  31. package/src/mock-readline.js +27 -0
  32. package/src/parser/dependencies.js +71 -0
  33. package/src/parser/markdown.js +505 -0
  34. package/src/parser/stream.js +96 -0
  35. package/src/prompts/modes/CODING.js +160 -0
  36. package/src/prompts/modes/GENERAL.js +105 -0
  37. package/src/prompts/modes/NETWORK.js +69 -0
  38. package/src/prompts/modes/SSH.js +53 -0
  39. package/src/prompts/systemPrompt.js +85 -0
  40. package/src/safety/check.js +210 -0
  41. package/src/services/crypto.js +78 -0
  42. package/src/services/executor.js +68 -0
  43. package/src/services/history.js +58 -0
  44. package/src/services/server-url.js +11 -0
  45. package/src/services/session.js +194 -0
  46. package/src/services/ssh.js +176 -0
  47. package/src/services/websocket.js +112 -0
  48. package/src/skills/loader.js +231 -0
  49. package/src/tools/browser.js +434 -0
  50. package/src/tools/local.js +1254 -0
  51. package/src/tools/mcp-client.js +209 -0
  52. package/src/tools/registry.js +132 -0
  53. package/src/tools/search-providers.js +237 -0
  54. package/src/tools/ssh.js +74 -0
  55. package/src/ui/App.js +2031 -0
  56. package/src/ui/animation.js +47 -0
  57. package/src/ui/components/AskUserDialog.js +33 -0
  58. package/src/ui/components/ConfirmationDialog.js +45 -0
  59. package/src/ui/components/DiffView.js +201 -0
  60. package/src/ui/components/Header.js +157 -0
  61. package/src/ui/components/HistoryPicker.js +130 -0
  62. package/src/ui/components/InputShell.js +22 -0
  63. package/src/ui/components/MessageHistory.js +1200 -0
  64. package/src/ui/components/ModalPanel.js +40 -0
  65. package/src/ui/components/ModePicker.js +161 -0
  66. package/src/ui/components/PlanDialog.js +48 -0
  67. package/src/ui/components/ProviderMenu.js +1095 -0
  68. package/src/ui/components/SavePicker.js +106 -0
  69. package/src/ui/components/SelectMenu.js +194 -0
  70. package/src/ui/components/SlashMenu.js +168 -0
  71. package/src/ui/components/SubagentPanel.js +138 -0
  72. package/src/ui/components/TextInputSafe.js +117 -0
  73. package/src/ui/components/TodoPanel.js +54 -0
  74. package/src/ui/components/ToolExecution.js +261 -0
  75. package/src/ui/components/TranscriptViewport.js +99 -0
  76. package/src/ui/diff.js +249 -0
  77. package/src/ui/h.js +7 -0
  78. package/src/ui/mouse-scroll.js +63 -0
  79. package/src/ui/slash-picker.js +58 -0
  80. package/src/ui/terminal.js +41 -0
  81. package/src/ui/theme.js +5 -0
  82. package/src/ui/welcome.js +12 -0
  83. package/src/utils/constants.js +231 -0
  84. package/src/utils/helpers.js +154 -0
  85. package/src/utils/logger.js +81 -0
  86. package/src/utils/sound.js +33 -0
package/src/ui/diff.js ADDED
@@ -0,0 +1,249 @@
1
+ import chalk from 'chalk';
2
+ import { highlightCode } from '../parser/markdown.js';
3
+
4
+ export const langFromPath = (fp) => {
5
+ if (!fp) return '';
6
+ const m = fp.match(/\.(\w+)$/);
7
+ if (!m) return '';
8
+ const ext = m[1].toLowerCase();
9
+ const map = {
10
+ js:'js', jsx:'jsx', ts:'ts', tsx:'tsx', mjs:'js', cjs:'js', mts:'ts', cts:'ts',
11
+ py:'py', python:'py', sh:'sh', bash:'sh', zsh:'sh', shell:'sh',
12
+ rs:'rs', go:'go', rb:'rb', java:'java', kt:'kt', swift:'swift',
13
+ css:'css', scss:'scss', less:'less', html:'html', htm:'html',
14
+ json:'json', xml:'xml', yaml:'yaml', yml:'yaml', md:'md',
15
+ sql:'sql', graphql:'graphql', gql:'graphql'
16
+ };
17
+ return map[ext] || '';
18
+ };
19
+
20
+ const c = {
21
+ removedBg: '#2d1010',
22
+ removedFg: '#f7768e',
23
+ addedBg: '#1a2f1a',
24
+ addedFg: '#9ece6a',
25
+ ctxFg: '#565f89',
26
+ lineNumFg: '#414868',
27
+ border: '#2a2e3f',
28
+ headerBg: '#1a1b26',
29
+ headerFg: '#7aa2f7',
30
+ dim: '#414868',
31
+ };
32
+
33
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
34
+ const visLen = (s) => stripAnsi(s).length;
35
+ const getWidth = () => Math.min(Math.max(process.stdout.columns || 96, 60), 120);
36
+ const wrapPlain = (line, width) => {
37
+ const safeWidth = Math.max(1, width | 0);
38
+ const src = String(line ?? '');
39
+ if (!src.length) return [''];
40
+ const out = [];
41
+ for (let i = 0; i < src.length; i += safeWidth) out.push(src.slice(i, i + safeWidth));
42
+ return out;
43
+ };
44
+
45
+ // ─── Unified diff between two strings ──────────────────────────
46
+ // Returns an array of { type: 'ctx'|'del'|'add', line, oldN, newN }
47
+ export const buildDiff = (oldText, newText) => {
48
+ const oldLines = (oldText || '').split('\n');
49
+ const newLines = (newText || '').split('\n');
50
+
51
+ // LCS-based Myers-lite diff (line level)
52
+ const m = oldLines.length, n = newLines.length;
53
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
54
+ for (let i = m - 1; i >= 0; i--)
55
+ for (let j = n - 1; j >= 0; j--)
56
+ dp[i][j] = oldLines[i] === newLines[j]
57
+ ? dp[i + 1][j + 1] + 1
58
+ : Math.max(dp[i + 1][j], dp[i][j + 1]);
59
+
60
+ const hunks = [];
61
+ let i = 0, j = 0;
62
+ while (i < m || j < n) {
63
+ if (i < m && j < n && oldLines[i] === newLines[j]) {
64
+ hunks.push({ type: 'ctx', line: oldLines[i], oldN: i + 1, newN: j + 1 });
65
+ i++; j++;
66
+ } else if (j < n && (i >= m || dp[i + 1] && dp[i][j + 1] >= (dp[i + 1] ? dp[i + 1][j] : 0))) {
67
+ hunks.push({ type: 'add', line: newLines[j], newN: j + 1 });
68
+ j++;
69
+ } else {
70
+ hunks.push({ type: 'del', line: oldLines[i], oldN: i + 1 });
71
+ i++;
72
+ }
73
+ }
74
+ return hunks;
75
+ };
76
+
77
+ // ─── Keep only changed hunks + 3 context lines around them ─────
78
+ export const collapseContext = (hunks, ctx = 3) => {
79
+ const changed = new Set();
80
+ hunks.forEach((h, i) => { if (h.type !== 'ctx') for (let d = -ctx; d <= ctx; d++) changed.add(i + d); });
81
+ const out = [];
82
+ let skipped = 0;
83
+ for (let i = 0; i < hunks.length; i++) {
84
+ if (changed.has(i)) {
85
+ if (skipped > 0) { out.push({ type: 'skip', count: skipped }); skipped = 0; }
86
+ out.push(hunks[i]);
87
+ } else {
88
+ skipped++;
89
+ }
90
+ }
91
+ if (skipped > 0) out.push({ type: 'skip', count: skipped });
92
+ return out;
93
+ };
94
+
95
+ // ─── Inline word-level highlighting for changed lines ──────────
96
+ const wordDiff = (oldLine, newLine, fg, bg) => {
97
+ const ow = oldLine.split(/(\s+)/);
98
+ const nw = newLine.split(/(\s+)/);
99
+ const oldSet = new Set(ow);
100
+ return nw.map(w => {
101
+ if (!oldSet.has(w) && w.trim()) return chalk.hex(fg).bgHex(bg).bold(w);
102
+ return chalk.hex(fg)(w);
103
+ }).join('');
104
+ };
105
+
106
+ // ─── Public: render a WRITE_FILE creation diff ─────────────────
107
+ export const printNewFile = (filePath, content) => {
108
+ const lines = (content || '').split('\n');
109
+ const w = getWidth();
110
+ const bc = chalk.hex(c.border);
111
+ const lnW = String(lines.length).length;
112
+ const innerW = w - lnW - 6;
113
+ const lang = langFromPath(filePath);
114
+
115
+ console.log();
116
+ const header = ` + ${filePath} (new file, ${lines.length} lines) `;
117
+ const headerPad = Math.max(0, w - 2 - header.length);
118
+ console.log(bc('╭') + chalk.bgHex(c.headerBg).hex(c.headerFg)(header) + chalk.hex(c.headerBg)('─'.repeat(headerPad)) + bc('╮'));
119
+
120
+ const shown = Math.min(lines.length, 50);
121
+ for (let i = 0; i < shown; i++) {
122
+ const lineNum = chalk.hex(c.lineNumFg)(String(i + 1).padStart(lnW));
123
+ const raw = lines[i].length > innerW ? lines[i].slice(0, innerW - 1) + '…' : lines[i];
124
+ const colored = highlightCode(raw, lang);
125
+ const prefix = chalk.bgHex(c.addedBg).hex(c.addedFg)('+');
126
+ const padLen = Math.max(0, innerW - stripAnsi(colored).length);
127
+ console.log(bc('│') + ' ' + lineNum + ' ' + prefix + ' ' + colored + ' '.repeat(padLen) + bc('│'));
128
+ }
129
+ if (lines.length > 50) {
130
+ const msg = ` ··· ${lines.length - 50} more lines`;
131
+ console.log(bc('│') + chalk.hex(c.dim)(msg) + ' '.repeat(Math.max(0, w - 2 - msg.length)) + bc('│'));
132
+ }
133
+ console.log(bc('╰' + '─'.repeat(w - 2) + '╯'));
134
+ console.log();
135
+ };
136
+
137
+ // ─── Public: render an EDIT_FILE diff (find → replace) ─────────
138
+ export const printEditDiff = (filePath, find, replace) => {
139
+ const hunks = buildDiff(find, replace);
140
+ const collapsed = collapseContext(hunks, 3);
141
+
142
+ const w = getWidth();
143
+ const bc = chalk.hex(c.border);
144
+ const allLines = [...hunks.filter(h => h.oldN || h.newN)];
145
+ const maxN = Math.max(...allLines.map(h => Math.max(h.oldN || 0, h.newN || 0)), 1);
146
+ const lnW = String(maxN).length;
147
+ const innerW = w - lnW * 2 - 7;
148
+ const lang = langFromPath(filePath);
149
+
150
+ console.log();
151
+ const header = ` ✎ ${filePath} `;
152
+ const headerPad = Math.max(0, w - 2 - header.length);
153
+ console.log(bc('╭') + chalk.bgHex(c.headerBg).hex(c.headerFg)(header) + chalk.hex(c.headerBg)('─'.repeat(headerPad)) + bc('╮'));
154
+
155
+ for (const hunk of collapsed) {
156
+ if (hunk.type === 'skip') {
157
+ const msg = ` ··· ${hunk.count} unchanged line${hunk.count > 1 ? 's' : ''}`;
158
+ const pad = Math.max(0, w - 2 - msg.length);
159
+ console.log(bc('│') + chalk.hex(c.dim)(msg) + ' '.repeat(pad) + bc('│'));
160
+ continue;
161
+ }
162
+
163
+ const oldNum = hunk.oldN ? String(hunk.oldN).padStart(lnW) : ' '.repeat(lnW);
164
+ const newNum = hunk.newN ? String(hunk.newN).padStart(lnW) : ' '.repeat(lnW);
165
+ const lineNums = chalk.hex(c.lineNumFg)(`${oldNum}:${newNum}`);
166
+ const lineNumsCont = chalk.hex(c.lineNumFg)(`${' '.repeat(lnW)}:${' '.repeat(lnW)}`);
167
+
168
+ if (hunk.type === 'ctx') {
169
+ const chunks = wrapPlain(hunk.line, innerW);
170
+ chunks.forEach((chunk, idx) => {
171
+ const nums = idx === 0 ? lineNums : lineNumsCont;
172
+ const styled = chalk.hex(c.ctxFg)(' ' + chunk);
173
+ const pad = Math.max(0, innerW + 1 - visLen(styled));
174
+ console.log(bc('│') + ' ' + nums + ' ' + styled + ' '.repeat(pad) + bc('│'));
175
+ });
176
+ } else if (hunk.type === 'del') {
177
+ const chunks = wrapPlain(hunk.line, innerW);
178
+ chunks.forEach((chunk, idx) => {
179
+ const nums = idx === 0 ? lineNums : lineNumsCont;
180
+ const sign = idx === 0 ? '-' : ' ';
181
+ const colored = chalk.bgHex(c.removedBg).hex(c.removedFg)(sign) + highlightCode(chunk, lang);
182
+ const pad = Math.max(0, innerW + 1 - stripAnsi(colored).length);
183
+ console.log(bc('│') + ' ' + nums + ' ' + colored + ' '.repeat(pad) + bc('│'));
184
+ });
185
+ } else if (hunk.type === 'add') {
186
+ const chunks = wrapPlain(hunk.line, innerW);
187
+ chunks.forEach((chunk, idx) => {
188
+ const nums = idx === 0 ? lineNums : lineNumsCont;
189
+ const sign = idx === 0 ? '+' : ' ';
190
+ const colored = chalk.bgHex(c.addedBg).hex(c.addedFg)(sign) + highlightCode(chunk, lang);
191
+ const pad = Math.max(0, innerW + 1 - stripAnsi(colored).length);
192
+ console.log(bc('│') + ' ' + nums + ' ' + colored + ' '.repeat(pad) + bc('│'));
193
+ });
194
+ }
195
+ }
196
+
197
+ const dels = hunks.filter(h => h.type === 'del').length;
198
+ const adds = hunks.filter(h => h.type === 'add').length;
199
+ const summary = ` ${chalk.hex(c.addedFg)('+' + adds)} ${chalk.hex(c.removedFg)('-' + dels)} `;
200
+ const summaryPad = Math.max(0, w - 2 - stripAnsi(summary).length);
201
+ console.log(bc('│') + summary + ' '.repeat(summaryPad) + bc('│'));
202
+ console.log(bc('╰' + '─'.repeat(w - 2) + '╯'));
203
+ console.log();
204
+ };
205
+
206
+ // ─── Public: render an APPEND_FILE diff ────────────────────────
207
+ export const printAppendDiff = (filePath, content) => {
208
+ const lines = (content || '').split('\n');
209
+ const w = getWidth();
210
+ const bc = chalk.hex(c.border);
211
+ const lnW = String(lines.length).length;
212
+ const innerW = w - lnW - 6;
213
+ const lang = langFromPath(filePath);
214
+
215
+ console.log();
216
+ const header = ` + ${filePath} (appending ${lines.length} line${lines.length > 1 ? 's' : ''}) `;
217
+ const headerPad = Math.max(0, w - 2 - header.length);
218
+ console.log(bc('╭') + chalk.bgHex(c.headerBg).hex(c.addedFg)(header) + chalk.hex(c.headerBg)('─'.repeat(headerPad)) + bc('╮'));
219
+
220
+ const skip = lines.length > 30;
221
+ const shown = skip ? lines.slice(0, 15) : lines;
222
+ shown.forEach((line, i) => {
223
+ const lineNum = chalk.hex(c.lineNumFg)(String(i + 1).padStart(lnW));
224
+ const raw = line.length > innerW ? line.slice(0, innerW - 1) + '…' : line;
225
+ const colored = highlightCode(raw, lang);
226
+ const pad = Math.max(0, innerW - stripAnsi(colored).length);
227
+ console.log(bc('│') + ' ' + lineNum + ' ' + chalk.bgHex(c.addedBg).hex(c.addedFg)('+') + ' ' + colored + ' '.repeat(pad) + bc('│'));
228
+ });
229
+ if (skip) {
230
+ const msg = ` ··· ${lines.length - 15} more lines`;
231
+ console.log(bc('│') + chalk.hex(c.dim)(msg) + ' '.repeat(Math.max(0, w - 2 - msg.length)) + bc('│'));
232
+ }
233
+ console.log(bc('╰' + '─'.repeat(w - 2) + '╯'));
234
+ console.log();
235
+ };
236
+
237
+ // ─── Public: DELETE_FILE ────────────────────────────────────────
238
+
239
+ // ─── Public: DELETE_FILE ────────────────────────────────────────
240
+ export const printDeleteFile = (filePath) => {
241
+ const w = getWidth();
242
+ const bc = chalk.hex(c.border);
243
+ const msg = ` ✗ ${filePath} (deleted) `;
244
+ const pad = Math.max(0, w - 2 - msg.length);
245
+ console.log();
246
+ console.log(bc('╭') + chalk.bgHex(c.removedBg).hex(c.removedFg)(msg) + chalk.hex(c.removedBg)('─'.repeat(pad)) + bc('╮'));
247
+ console.log(bc('╰' + '─'.repeat(w - 2) + '╯'));
248
+ console.log();
249
+ };
package/src/ui/h.js ADDED
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Helper to use React.createElement concisely instead of JSX,
5
+ * avoiding the need for a build step in this ESM project.
6
+ */
7
+ export const h = React.createElement;
@@ -0,0 +1,63 @@
1
+ const SGR_MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([mM])/g;
2
+ const SGR_MOUSE_INPUT_STRIP_RE = /\[<\d+;\d+;\d+[mM]/g;
3
+ const SGR_MOUSE_INPUT_DETECT_RE = /\[<\d+;\d+;\d+[mM]/;
4
+
5
+ /** True when stdin chunk is only SGR mouse events (clicks/wheel), not real text. */
6
+ export function isOnlySgrMouseInput(input) {
7
+ if (!input) return false;
8
+ const stripped = input
9
+ .replace(/\x1b\[<\d+;\d+;\d+[mM]/g, '')
10
+ .replace(SGR_MOUSE_INPUT_STRIP_RE, '');
11
+ return stripped.length === 0 && SGR_MOUSE_INPUT_DETECT_RE.test(input);
12
+ }
13
+ let WHEEL_LINES = 3;
14
+ if (process.env.OSAI_UI_WHEEL_LINES) {
15
+ const n = Number.parseInt(process.env.OSAI_UI_WHEEL_LINES, 10);
16
+ if (Number.isFinite(n) && n > 0) WHEEL_LINES = n;
17
+ }
18
+
19
+ export const MOUSE_SCROLL_ENABLED = process.env.OSAI_UI_MOUSE_SCROLL !== '0';
20
+
21
+ export function enableMouseReporting() {
22
+ if (!MOUSE_SCROLL_ENABLED || !process.stdout.isTTY) return;
23
+ try {
24
+ process.stdout.write('\x1b[?1000h'); // Enable basic mouse tracking (press events incl. wheel)
25
+ process.stdout.write('\x1b[?1006h'); // Enable SGR mouse encoding (coordinates > 223)
26
+ } catch {}
27
+ }
28
+
29
+ export function disableMouseReporting() {
30
+ if (!process.stdout.isTTY) return;
31
+ try {
32
+ process.stdout.write('\x1b[?1006l'); // Disable SGR mouse encoding
33
+ process.stdout.write('\x1b[?1000l'); // Disable mouse tracking
34
+ } catch {}
35
+ }
36
+
37
+ /**
38
+ * Listen for SGR mouse wheel events on stdin.
39
+ * @param {(delta: number) => void} onWheel negative = up, positive = down
40
+ * @returns {() => void} cleanup
41
+ */
42
+ export function setupMouseScroll(onWheel) {
43
+ if (!MOUSE_SCROLL_ENABLED || !process.stdin.isTTY) return () => {};
44
+
45
+ enableMouseReporting();
46
+
47
+ const onData = (chunk) => {
48
+ const str = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
49
+ let match;
50
+ SGR_MOUSE_RE.lastIndex = 0;
51
+ while ((match = SGR_MOUSE_RE.exec(str)) !== null) {
52
+ const btn = Number.parseInt(match[1], 10);
53
+ if (btn === 64) onWheel(-WHEEL_LINES);
54
+ else if (btn === 65) onWheel(WHEEL_LINES);
55
+ }
56
+ };
57
+
58
+ process.stdin.on('data', onData);
59
+ return () => {
60
+ process.stdin.off('data', onData);
61
+ disableMouseReporting();
62
+ };
63
+ }
@@ -0,0 +1,58 @@
1
+ import { printBoxHeaderOnly } from './terminal.js';
2
+ import chalk from 'chalk';
3
+
4
+ export const SLASH_COMMANDS = [
5
+ { cmd: '/clear', desc: 'Clear displayed history', action: 'clear' },
6
+ { cmd: '/help', desc: 'Show available commands', action: 'help' },
7
+ { cmd: '/new', desc: 'Start a new section (saves current)', action: 'new' },
8
+ { cmd: '/status', desc: 'Show OS, mode and session info', action: 'status' },
9
+ { cmd: '/stats', desc: 'Iterations, tokens and timing', action: 'stats' },
10
+ { cmd: '/history', desc: 'Saved sessions - resume a past conversation', action: 'history' },
11
+ { cmd: '/save', desc: 'Save current session to disk', action: 'save' },
12
+ { cmd: '/context', desc: 'Show conversation context size', action: 'context' },
13
+ { cmd: '/devices', desc: 'List configured devices', action: 'devices' },
14
+ { cmd: '/provider', desc: 'Manage AI providers and API keys', action: 'provider' },
15
+ { cmd: '/plan', desc: 'Switch to PLAN mode (read-only, no modifications)', action: 'plan' },
16
+ { cmd: '/exec', desc: 'Switch to EXEC mode (modifications allowed)', action: 'exec' },
17
+ { cmd: '/mode', desc: 'Switch GENERAL / CODING / NETWORK / SSH mode', action: 'mode' },
18
+ { cmd: '/todos', desc: 'Show current todo list', action: 'todos' },
19
+ { cmd: '/skills', desc: 'List available agent skills', action: 'skills' },
20
+ { cmd: '/exit', desc: 'Exit the agent', action: 'exit' },
21
+ { cmd: '/logout', desc: 'Log out and clear credentials', action: 'logout' },
22
+ ];
23
+
24
+ export const MODE_OPTIONS = [
25
+ { name: 'GENERAL', desc: 'System administration mode', value: 'GENERAL' },
26
+ { name: 'CODING', desc: 'Software engineering mode', value: 'CODING' },
27
+ { name: 'NETWORK', desc: 'Remote network device management', value: 'NETWORK' },
28
+ { name: 'SSH', desc: 'Remote execution via SSH', value: 'SSH' },
29
+ { name: 'PLAN', desc: 'Plan mode — read-only, no file modifications', value: 'PLAN' },
30
+ { name: 'EXEC', desc: 'Exec mode — file modifications allowed', value: 'EXEC' },
31
+ ];
32
+
33
+ export const executeSlashCommand = async (cmd, rl, userOS) => {
34
+ switch (cmd) {
35
+ case '/clear': return { action: 'clear' };
36
+ case '/help': return { action: 'help' };
37
+ case '/new': return { action: 'new' };
38
+ case '/exit': return { action: 'exit' };
39
+ case '/devices': return { action: 'devices' };
40
+ case '/provider': return { action: 'provider' };
41
+ case '/todos': return { action: 'todos' };
42
+ case '/skills': return { action: 'skills' };
43
+ case '/status': return { action: 'status' };
44
+ case '/history': return { action: 'history' };
45
+ case '/save': return { action: 'save' };
46
+ case '/stats': return { action: 'stats' };
47
+ case '/context': return { action: 'context' };
48
+ case '/plan': return { action: 'plan' };
49
+ case '/exec': return { action: 'exec' };
50
+ case '/mode': return { action: 'show_mode_picker' };
51
+ case '/logout': return { action: 'logout' };
52
+ default: return { action: 'none' };
53
+ }
54
+ };
55
+
56
+ export const showModePicker = async () => {
57
+ return { action: 'none' };
58
+ };
@@ -0,0 +1,41 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+
4
+ export const printInfo = (msg) => console.log(chalk.blue('i ') + msg);
5
+ export const printError = (msg) => console.error(chalk.red('x ') + msg);
6
+ export const printSuccess = (msg) => console.log(chalk.green('✓ ') + msg);
7
+ export const printHeader = () => {};
8
+ export const printAuthHeader = (title, subtitle = '') => {
9
+ console.log(boxen(
10
+ chalk.hex('#7aa2f7').bold(` ${title}`) +
11
+ (subtitle ? chalk.hex('#565f89').dim(`\n ${subtitle}`) : ''),
12
+ { padding: 1, borderStyle: 'round', borderColor: '#4a9eff' }
13
+ ));
14
+ };
15
+ export const clearScreen = () => { process.stdout.write('\x1Bc'); };
16
+ export const printDeviceList = () => {};
17
+ export const renderInputFooter = () => {};
18
+ export const printBoxHeaderOnly = () => {};
19
+ export const printBoxedMessage = () => {};
20
+ export const printShell = (msg) => console.log(msg);
21
+ export const printPanel = (title, lines, colorFn) => {
22
+ const content = lines.join('\n');
23
+ const coloredTitle = colorFn ? colorFn(title) : title;
24
+ console.log(boxen(content, {
25
+ padding: { left: 2, right: 2 },
26
+ borderStyle: 'round',
27
+ borderColor: '#4a9eff',
28
+ title: coloredTitle,
29
+ titleAlignment: 'left',
30
+ float: 'left',
31
+ }));
32
+ };
33
+ export const printNotLoggedIn = () => {
34
+ console.log(boxen(
35
+ chalk.hex('#ff9e64')(' You are not logged in') +
36
+ chalk.hex('#c0caf5')('\n\n Please authenticate:') +
37
+ chalk.white('\n osai-agent login ') + chalk.gray('Log in') +
38
+ chalk.white('\n osai-agent register ') + chalk.gray('Create an account'),
39
+ { padding: 1, borderStyle: 'round', borderColor: '#ff9e64' }
40
+ ));
41
+ };
@@ -0,0 +1,5 @@
1
+ /** Fond cendre sombre pour les zones de saisie secondaires */
2
+ export const INPUT_BACKGROUND = '#2b2d38';
3
+
4
+ /** Fond noir cendré clair pour la saisie principale */
5
+ export const MAIN_INPUT_BACKGROUND = '#202127';
@@ -0,0 +1,12 @@
1
+ import { APP_VERSION } from '../utils/constants.js';
2
+
3
+ export const LOGO_LINES = [
4
+ ' ██████ ███████ █████ ██ █████ ██████ ███████ ███ ██ ████████',
5
+ ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ',
6
+ ' ██ ██ ███████ ███████ ██ ███████ ██ ███ █████ ██ ██ ██ ██ ',
7
+ ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
8
+ ' ██████ ███████ ██ ██ ██ ██ ██ ██████ ███████ ██ ████ ██ ',
9
+ ];
10
+
11
+ export const TAGLINE = 'Your AI sysadmin, network engineer & senior developer \u2014 all in one CLI';
12
+ export const VERSION = `v${APP_VERSION}`;
@@ -0,0 +1,231 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Constants & Configuration
3
+ // =============================================================================
4
+
5
+ export const APP_NAME = 'osai-agent';
6
+ export const APP_VERSION = '4.0.0';
7
+
8
+ /** Default runtime parameters — can be overridden via environment variables */
9
+ export const DEFAULTS = {
10
+ MAX_ITERATIONS: 100,
11
+ AUTO_CONTINUE_LIMIT: 10,
12
+ COMMAND_TIMEOUT: 30000,
13
+ FILE_READ_TIMEOUT: 10000,
14
+ MAX_OUTPUT_LINES: 50,
15
+ MAX_TOOL_RESULT_LENGTH: 12000,
16
+ WS_RECONNECT_MAX_DELAY: 20000,
17
+ WS_RECONNECT_BASE_DELAY: 1000,
18
+ WS_RECONNECT_MAX_ATTEMPTS: 5,
19
+ SSH_INACTIVITY_TIMEOUT: 300000,
20
+ SSH_COMMAND_TIMEOUT: 30000,
21
+ API_RETRY_ATTEMPTS: 5,
22
+ API_RETRY_DELAY: 1500,
23
+ MAX_COMMAND_HISTORY: 1000,
24
+ MAX_MEMORY_ENTRIES: 500,
25
+ AGENT_LOOP_RETRY: 2,
26
+ CONTEXT_COMPACT_THRESHOLD: 0.8,
27
+ ESTIMATED_CONTEXT_WINDOW: 128000,
28
+ FETCH_TIMEOUT: 15000,
29
+ MAX_TODOS: 50,
30
+ };
31
+
32
+ /** Tool identifiers used in JSON tool calls from the LLM */
33
+ export const TOOLS = {
34
+ // Existing tools
35
+ LOCAL_CMD: 'LOCAL_CMD',
36
+ SSH_CMD: 'SSH_CMD',
37
+ READ_FILE: 'READ_FILE',
38
+ WRITE_FILE: 'WRITE_FILE',
39
+ EDIT_FILE: 'EDIT_FILE',
40
+ LIST_DIR: 'LIST_DIR',
41
+ // Legacy — mapped to LOCAL_CMD in extractToolCalls, not exposed in prompts
42
+ POWERSHELL: 'POWERSHELL',
43
+ // Coding tools
44
+ SEARCH_FILE: 'SEARCH_FILE',
45
+ APPEND_FILE: 'APPEND_FILE',
46
+ DELETE_FILE: 'DELETE_FILE',
47
+ CREATE_DIR: 'CREATE_DIR',
48
+ TREE_VIEW: 'TREE_VIEW',
49
+ RUN_SCRIPT: 'RUN_SCRIPT',
50
+ MOVE_FILE: 'MOVE_FILE',
51
+ COPY_FILE: 'COPY_FILE',
52
+ FILE_INFO: 'FILE_INFO',
53
+ // Web tools
54
+ FETCH_URL: 'FETCH_URL',
55
+ WEB_SEARCH: 'WEB_SEARCH',
56
+ // Browser tools (Playwright + HTTP fallback)
57
+ BROWSE: 'BROWSE',
58
+ BROWSE_SEARCH: 'BROWSE_SEARCH',
59
+ BROWSE_EXTRACT: 'BROWSE_EXTRACT',
60
+ // Todo tools
61
+ TODO_ADD: 'TODO_ADD',
62
+ TODO_COMPLETE: 'TODO_COMPLETE',
63
+ TODO_UPDATE: 'TODO_UPDATE',
64
+ TODO_LIST: 'TODO_LIST',
65
+ TODO_CLEAR: 'TODO_CLEAR',
66
+ // New coding tools
67
+ GLOB: 'GLOB',
68
+ GREP: 'GREP',
69
+ GIT: 'GIT',
70
+ DIAG_POST_EDIT: 'DIAG_POST_EDIT',
71
+ ASK_USER: 'ASK_USER',
72
+ PLAN_MODE: 'PLAN_MODE',
73
+ DIR_CHANGE: 'DIR_CHANGE',
74
+ GET_DEPENDENCIES: 'GET_DEPENDENCIES',
75
+ MCP_TOOL: 'MCP_TOOL',
76
+ // Skills & subagent
77
+ SKILL_LIST: 'SKILL_LIST',
78
+ LOAD_SKILL: 'LOAD_SKILL',
79
+ CREATE_SKILL: 'CREATE_SKILL',
80
+ TASK: 'TASK',
81
+ };
82
+
83
+ /** Max iterations for a read-only subagent */
84
+ export const SUBAGENT_MAX_ITERATIONS = parseInt(process.env.OSAI_SUBAGENT_MAX_ITERATIONS) || 25;
85
+
86
+ /** Completion signals that the LLM uses to indicate task status */
87
+ export const COMPLETION_SIGNALS = {
88
+ DONE: 'DONE',
89
+ INCOMPLETE: 'INCOMPLETE',
90
+ BLOCKED: 'BLOCKED',
91
+ };
92
+
93
+ /** Safety tier classifications for command risk assessment */
94
+ export const SAFETY_TIERS = {
95
+ READ: 'READ',
96
+ WRITE: 'WRITE',
97
+ DANGEROUS: 'DANGEROUS',
98
+ ASK: 'ASK',
99
+ };
100
+
101
+ /** Agent execution modes */
102
+ export const MODES = {
103
+ SSH: 'SSH',
104
+ GENERAL: 'GENERAL',
105
+ NETWORK: 'NETWORK',
106
+ CODING: 'CODING',
107
+ };
108
+
109
+ /** Execution sub-modes — PLAN (read-only, no modifications) / EXEC (normal) */
110
+ export const EXECUTION_MODES = {
111
+ PLAN: 'PLAN',
112
+ EXEC: 'EXEC',
113
+ };
114
+
115
+ /** Environment variable names the agent recognises */
116
+ export const ENV_VARS = {
117
+ DEBUG: 'OSAI_DEBUG',
118
+ VERBOSE: 'OSAI_VERBOSE',
119
+ MAX_ITERATIONS: 'OSAI_MAX_ITERATIONS',
120
+ AUTO_CONTINUE: 'OSAI_AUTO_CONTINUE',
121
+ COMMAND_TIMEOUT: 'OSAI_COMMAND_TIMEOUT',
122
+ AES_SECRET: 'AES_SECRET',
123
+ JSON_OUTPUT: 'OSAI_JSON',
124
+ };
125
+
126
+ /** Set of tool names that are inherently read-only (no confirmation needed) */
127
+ export const READ_ONLY_TOOLS = new Set([
128
+ TOOLS.READ_FILE, TOOLS.LIST_DIR, TOOLS.SEARCH_FILE,
129
+ TOOLS.TREE_VIEW, TOOLS.FILE_INFO, TOOLS.FETCH_URL,
130
+ TOOLS.WEB_SEARCH, TOOLS.TODO_LIST,
131
+ TOOLS.GLOB, TOOLS.GREP, TOOLS.DIAG_POST_EDIT, TOOLS.ASK_USER, TOOLS.PLAN_MODE,
132
+ TOOLS.BROWSE, TOOLS.BROWSE_SEARCH, TOOLS.BROWSE_EXTRACT,
133
+ TOOLS.DIR_CHANGE,
134
+ TOOLS.GET_DEPENDENCIES,
135
+ TOOLS.MCP_TOOL,
136
+ TOOLS.SKILL_LIST, TOOLS.LOAD_SKILL, TOOLS.CREATE_SKILL, TOOLS.TASK,
137
+ ]);
138
+
139
+ /** Tools allowed inside a read-only subagent */
140
+ export const SUBAGENT_ALLOWED_TOOLS = new Set([
141
+ TOOLS.READ_FILE, TOOLS.LIST_DIR, TOOLS.SEARCH_FILE, TOOLS.TREE_VIEW, TOOLS.FILE_INFO,
142
+ TOOLS.FETCH_URL, TOOLS.WEB_SEARCH, TOOLS.GLOB, TOOLS.GREP, TOOLS.GET_DEPENDENCIES,
143
+ TOOLS.BROWSE, TOOLS.BROWSE_SEARCH, TOOLS.BROWSE_EXTRACT,
144
+ TOOLS.SKILL_LIST, TOOLS.LOAD_SKILL,
145
+ TOOLS.LOCAL_CMD, TOOLS.GIT,
146
+ ]);
147
+
148
+ /** Write tools that modify files but are safe in coding context */
149
+ export const CODING_TOOLS = new Set([
150
+ TOOLS.LOCAL_CMD, TOOLS.READ_FILE, TOOLS.WRITE_FILE, TOOLS.EDIT_FILE,
151
+ TOOLS.LIST_DIR, TOOLS.SEARCH_FILE, TOOLS.APPEND_FILE, TOOLS.DELETE_FILE,
152
+ TOOLS.CREATE_DIR, TOOLS.TREE_VIEW, TOOLS.RUN_SCRIPT, TOOLS.MOVE_FILE,
153
+ TOOLS.COPY_FILE, TOOLS.FILE_INFO, TOOLS.FETCH_URL, TOOLS.WEB_SEARCH,
154
+ TOOLS.TODO_ADD, TOOLS.TODO_COMPLETE, TOOLS.TODO_UPDATE, TOOLS.TODO_LIST,
155
+ TOOLS.TODO_CLEAR, TOOLS.GLOB, TOOLS.GREP, TOOLS.GIT, TOOLS.DIAG_POST_EDIT,
156
+ TOOLS.ASK_USER, TOOLS.PLAN_MODE,
157
+ TOOLS.BROWSE, TOOLS.BROWSE_SEARCH, TOOLS.BROWSE_EXTRACT,
158
+ TOOLS.DIR_CHANGE,
159
+ TOOLS.MCP_TOOL,
160
+ TOOLS.CREATE_SKILL,
161
+ TOOLS.TASK,
162
+ ]);
163
+
164
+ /** All known tool names for regex extraction */
165
+ export const ALL_TOOL_NAMES = Object.values(TOOLS);
166
+
167
+ /** Cross-platform shell configuration */
168
+ export const SHELL = {
169
+ windows: { name: 'cmd.exe', flag: '/c' },
170
+ macos: { name: '/bin/zsh', flag: '-c' },
171
+ linux: { name: '/bin/sh', flag: '-c' },
172
+ };
173
+
174
+ /** Filenames explicitly excluded from critical env detection (safe to read) */
175
+ export const CRITICAL_ENV_EXCLUDE = new Set([
176
+ '.env.example',
177
+ ]);
178
+
179
+ /** Patterns for critical environment variable / secret files */
180
+ export const CRITICAL_ENV_FILENAMES = new Set([
181
+ '.env', '.env.local', '.env.development', '.env.production',
182
+ '.env.staging', '.env.test', '.env.docker', '.env.cloud',
183
+ 'credentials.json', 'credentials.yml', 'credentials.yaml',
184
+ 'secrets.json', 'secrets.yml', 'secrets.yaml', 'secret.json',
185
+ '.netrc', '_netrc', '.npmrc', '.yarnrc.yml',
186
+ 'id_rsa', 'id_rsa.pub', 'id_ed25519', 'id_ed25519.pub',
187
+ 'authorized_keys', 'known_hosts',
188
+ 'config.json',
189
+ ]);
190
+
191
+ export const CRITICAL_ENV_FILENAME_PATTERNS = [
192
+ /^\.env\b/,
193
+ /\.pem$/i,
194
+ /\.key$/i,
195
+ /^id_rsa/,
196
+ /^id_ed25519/,
197
+ /^application_default_credentials\.json$/i,
198
+ /^credentials\.(json|yml|yaml)$/i,
199
+ /^secrets\.(json|yml|yaml)$/i,
200
+ ];
201
+
202
+ export const CRITICAL_ENV_PATH_SEGMENTS = [
203
+ '.config', '.aws', '.azure', '.gcloud', '.kube', '.docker',
204
+ 'credentials', 'secrets',
205
+ ];
206
+
207
+ /** Device type categories for network mode */
208
+ export const DEVICE_TYPES = {
209
+ CISCO_IOS: 'Cisco IOS',
210
+ CISCO_IOS_XE: 'Cisco IOS-XE',
211
+ CISCO_NX_OS: 'Cisco NX-OS',
212
+ CISCO_ASA: 'Cisco ASA',
213
+ MIKROTIK: 'MikroTik RouterOS',
214
+ PFSENSE: 'pfSense',
215
+ JUNOS: 'Juniper JunOS',
216
+ LINUX: 'Linux',
217
+ WINDOWS: 'Windows Server',
218
+ };
219
+
220
+ // Agent loop constants
221
+ export const MAX_ITERATIONS = parseInt(process.env.OSAI_MAX_ITERATIONS) || DEFAULTS.MAX_ITERATIONS;
222
+ export const AUTO_CONTINUE_LIMIT = parseInt(process.env.OSAI_AUTO_CONTINUE) || DEFAULTS.AUTO_CONTINUE_LIMIT;
223
+ export const READ_FRESHNESS_INTERACTIONS = 20;
224
+ export const MAX_PERSISTED_READ_FILES = 200;
225
+ export const TOOL_SEQUENCE_WINDOW_SIZE = 6;
226
+ export const HISTORY_SUMMARY_TRIGGER_TOKENS = 9600;
227
+ export const HISTORY_SUMMARY_KEEP_RECENT_TOKENS = 2000;
228
+ export const HISTORY_SUMMARY_MIN_GROWTH_TOKENS = 1000;
229
+ export const MAX_CONTEXT_TOKENS = 12800;
230
+ export const MAX_CONTEXT_SUMMARY_CHARS = 4000;
231
+ export const CONTEXT_SUMMARY_TAG = '[CONTEXT_SUMMARY]';