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
@@ -0,0 +1,154 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Shared Helpers
3
+ // =============================================================================
4
+
5
+ /**
6
+ * Estimate token count from text (rough: ~4 chars per token)
7
+ */
8
+ export const estimateTokens = (text) => Math.ceil((text || '').length / 4);
9
+
10
+ /**
11
+ * Truncate text to a maximum character length with an ellipsis suffix
12
+ */
13
+ export const truncateText = (text, maxLen = 8000) => {
14
+ if (!text || text.length <= maxLen) return text;
15
+ return text.slice(0, maxLen) + '\n...[output truncated]';
16
+ };
17
+
18
+ /**
19
+ * Wait for a specified number of milliseconds
20
+ */
21
+ export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
22
+
23
+ /**
24
+ * Sleep that can be interrupted when shouldCancel() returns true (e.g. user pressed Esc).
25
+ */
26
+ export const cancellableSleep = async (ms, shouldCancel, stepMs = 100) => {
27
+ let elapsed = 0;
28
+ while (elapsed < ms) {
29
+ if (shouldCancel?.()) return false;
30
+ const wait = Math.min(stepMs, ms - elapsed);
31
+ await sleep(wait);
32
+ elapsed += wait;
33
+ }
34
+ return !shouldCancel?.();
35
+ };
36
+
37
+ /**
38
+ * Retry an async operation with exponential backoff and jitter
39
+ * Based on production best practices from Claude Code / Cursor research
40
+ */
41
+ export const retryAsync = async (fn, { maxAttempts = 3, baseDelay = 1000, jitter = true, label = 'operation' } = {}) => {
42
+ let lastError = null;
43
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
44
+ try {
45
+ return { success: true, result: await fn(attempt) };
46
+ } catch (error) {
47
+ lastError = error;
48
+ if (attempt < maxAttempts - 1) {
49
+ // Exponential backoff with jitter to avoid thundering herd
50
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
51
+ const jitterAmount = jitter ? Math.random() * baseDelay * 0.5 : 0;
52
+ const delay = exponentialDelay + jitterAmount;
53
+ console.error(`[RETRY] ${label} attempt ${attempt + 1}/${maxAttempts}, retrying in ${Math.round(delay)}ms...`);
54
+ await sleep(delay);
55
+ }
56
+ }
57
+ }
58
+ return { success: false, error: lastError };
59
+ };
60
+
61
+ /**
62
+ * Categorize errors for appropriate handling
63
+ * Based on research: recoverable / requires-input / terminal / fatal
64
+ */
65
+ export const categorizeError = (error) => {
66
+ const msg = (error?.message || '').toLowerCase();
67
+
68
+ // Fatal errors — should stop the agent loop
69
+ if (msg.includes('not logged in') || msg.includes('authentication') || msg.includes('invalid token')) {
70
+ return { category: 'fatal', recoverable: false, userMessage: error.message };
71
+ }
72
+
73
+ // Terminal errors — cannot proceed
74
+ if (msg.includes('maximum iterations') || msg.includes('quota') || msg.includes('rate limit')) {
75
+ return { category: 'terminal', recoverable: false, userMessage: error.message };
76
+ }
77
+
78
+ // Requires user input — pass to LLM for re-evaluation
79
+ if (msg.includes('cancelled by user') || msg.includes('permission denied') || msg.includes('access denied')) {
80
+ return { category: 'requires-input', recoverable: true, userMessage: error.message };
81
+ }
82
+
83
+ // Recoverable errors — can be retried
84
+ if (msg.includes('timeout') || msg.includes('econnrefused') || msg.includes('network') || msg.includes('fetch')) {
85
+ return { category: 'recoverable', recoverable: true, userMessage: `Temporary error: ${error.message}` };
86
+ }
87
+
88
+ // Default — treat as recoverable and let the LLM adapt
89
+ return { category: 'recoverable', recoverable: true, userMessage: error.message };
90
+ };
91
+
92
+ /**
93
+ * Sanitize a string for safe shell usage (basic escaping)
94
+ */
95
+ export const sanitizeForShell = (str) => {
96
+ if (typeof str !== 'string') return '';
97
+ return str.replace(/[`$\\]/g, '\\$&');
98
+ };
99
+
100
+ /**
101
+ * Format bytes to human-readable string
102
+ */
103
+ export const formatBytes = (bytes) => {
104
+ if (bytes === 0) return '0 B';
105
+ const k = 1024;
106
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
107
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
108
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
109
+ };
110
+
111
+ /**
112
+ * Format milliseconds to human-readable duration
113
+ */
114
+ export const formatDuration = (ms) => {
115
+ if (ms < 1000) return `${ms}ms`;
116
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
117
+ const minutes = Math.floor(ms / 60000);
118
+ const seconds = ((ms % 60000) / 1000).toFixed(0);
119
+ return `${minutes}m ${seconds}s`;
120
+ };
121
+
122
+ /**
123
+ * Create a promise that rejects after a timeout
124
+ */
125
+ export const withTimeout = (promise, ms, label = 'Operation') => {
126
+ return Promise.race([
127
+ promise,
128
+ new Promise((_, reject) =>
129
+ setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1000}s`)), ms)
130
+ ),
131
+ ]);
132
+ };
133
+
134
+ /**
135
+ * Generate a simple unique ID (timestamp + random)
136
+ * Used as lightweight alternative to uuid for non-cryptographic purposes
137
+ */
138
+ export const generateId = (prefix = '') => {
139
+ const now = new Date();
140
+ const ts = now.getTime().toString(36);
141
+ const rand = Math.random().toString(36).slice(2, 8);
142
+ return prefix ? `${prefix}_${ts}_${rand}` : `${ts}_${rand}`;
143
+ };
144
+
145
+ /**
146
+ * Simple hash function for content fingerprinting
147
+ */
148
+ export const simpleHash = (str) => {
149
+ let h = 0;
150
+ for (let i = 0; i < str.length; i++) {
151
+ h = Math.imul(31, h) + str.charCodeAt(i) | 0;
152
+ }
153
+ return h.toString(36);
154
+ };
@@ -0,0 +1,81 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Logger
3
+ // =============================================================================
4
+ import chalk from 'chalk';
5
+ import { ENV_VARS } from './constants.js';
6
+
7
+ const LOG_LEVELS = { debug: 0, verbose: 1, info: 2, warn: 3, error: 4, silent: 5 };
8
+
9
+ let currentLevel = LOG_LEVELS.info;
10
+ let jsonOutput = false;
11
+ let silent = false;
12
+
13
+ const subscribers = [];
14
+
15
+ export function subscribeToLogs(callback) {
16
+ subscribers.push(callback);
17
+ return () => {
18
+ const idx = subscribers.indexOf(callback);
19
+ if (idx >= 0) subscribers.splice(idx, 1);
20
+ };
21
+ }
22
+
23
+ function notify(level, msg, data) {
24
+ const entry = { level, msg, data, timestamp: Date.now() };
25
+ subscribers.forEach(cb => { try { cb(entry); } catch {} });
26
+ }
27
+
28
+ // Configure from environment
29
+ if (process.env[ENV_VARS.DEBUG] === 'true') {
30
+ currentLevel = LOG_LEVELS.debug;
31
+ } else if (process.env[ENV_VARS.VERBOSE] === 'true') {
32
+ currentLevel = LOG_LEVELS.verbose;
33
+ }
34
+
35
+ if (process.env[ENV_VARS.JSON_OUTPUT] === 'true') {
36
+ jsonOutput = true;
37
+ }
38
+
39
+ const timestamp = () => new Date().toISOString();
40
+
41
+ const formatMsg = (level, msg, data = null) => {
42
+ if (jsonOutput) {
43
+ return JSON.stringify({ level, timestamp: timestamp(), message: msg, ...(data ? { data } : {}) });
44
+ }
45
+ const prefix = chalk.hex('#414868')(`[${timestamp().slice(11, 19)}]`);
46
+ const levelColors = {
47
+ debug: chalk.hex('#565f89'),
48
+ verbose: chalk.hex('#7dcfff'),
49
+ info: chalk.hex('#7aa2f7'),
50
+ warn: chalk.hex('#e0af68'),
51
+ error: chalk.hex('#f7768e'),
52
+ };
53
+ const levelStr = levelColors[level] ? levelColors[level](`[${level.toUpperCase()}]`) : `[${level.toUpperCase()}]`;
54
+ const dataStr = data ? chalk.hex('#3b3f52')(` ${JSON.stringify(data)}`) : '';
55
+ return `${prefix} ${levelStr} ${msg}${dataStr}`;
56
+ };
57
+
58
+ export const logger = {
59
+ debug(msg, data) {
60
+ if (currentLevel <= LOG_LEVELS.debug) { if (!silent) console.error(formatMsg('debug', msg, data)); notify('debug', msg, data); }
61
+ },
62
+ verbose(msg, data) {
63
+ if (currentLevel <= LOG_LEVELS.verbose) { if (!silent) console.error(formatMsg('verbose', msg, data)); notify('verbose', msg, data); }
64
+ },
65
+ info(msg, data) {
66
+ if (currentLevel <= LOG_LEVELS.info) { if (!silent) console.error(formatMsg('info', msg, data)); notify('info', msg, data); }
67
+ },
68
+ warn(msg, data) {
69
+ if (currentLevel <= LOG_LEVELS.warn) { if (!silent) console.error(formatMsg('warn', msg, data)); notify('warn', msg, data); }
70
+ },
71
+ error(msg, data) {
72
+ if (currentLevel <= LOG_LEVELS.error) { if (!silent) console.error(formatMsg('error', msg, data)); notify('error', msg, data); }
73
+ },
74
+ setLevel(level) {
75
+ if (LOG_LEVELS[level] !== undefined) currentLevel = LOG_LEVELS[level];
76
+ },
77
+ setSilent(v) { silent = v; },
78
+ isDebug() { return currentLevel <= LOG_LEVELS.debug; },
79
+ isVerbose() { return currentLevel <= LOG_LEVELS.verbose; },
80
+ enableJson() { jsonOutput = true; },
81
+ };
@@ -0,0 +1,33 @@
1
+ // =============================================================================
2
+ // OS AI Agent — System Sound Notification
3
+ // =============================================================================
4
+
5
+ /**
6
+ * Play a system beep sound (cross-platform: Windows, macOS, Linux)
7
+ * This uses the terminal bell character which works on all platforms
8
+ * without any external dependencies or visible output
9
+ */
10
+ export const playSystemBeep = () => {
11
+ try {
12
+ // Use the terminal bell character (ASCII 0x07)
13
+ // This works on Windows, macOS, and Linux terminals
14
+ process.stdout.write('\x07');
15
+ } catch (error) {
16
+ // Silently fail if beep cannot be played
17
+ // This ensures the application continues normally
18
+ }
19
+ };
20
+
21
+ /**
22
+ * Play system beep only if user has been inactive for a specified time
23
+ * @param {number} lastActivityTimestamp - Timestamp of last user activity
24
+ * @param {number} inactivityThresholdMs - Threshold in milliseconds (default: 600000ms = 10min)
25
+ */
26
+ export const playSystemBeepIfInactive = (lastActivityTimestamp, inactivityThresholdMs = 600000) => {
27
+ const now = Date.now();
28
+ const inactiveTime = now - lastActivityTimestamp;
29
+
30
+ if (inactiveTime >= inactivityThresholdMs) {
31
+ playSystemBeep();
32
+ }
33
+ };