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.
- package/LICENSE +7 -0
- package/package.json +72 -0
- package/src/agent/context.js +141 -0
- package/src/agent/loop/context-summary.js +196 -0
- package/src/agent/loop/directory-utils.js +102 -0
- package/src/agent/loop/local.js +196 -0
- package/src/agent/loop/loop-detection.js +288 -0
- package/src/agent/loop/stream-parser.js +515 -0
- package/src/agent/loop/tool-executor.js +470 -0
- package/src/agent/loop/verification.js +263 -0
- package/src/agent/loop/websocket.js +80 -0
- package/src/agent/prompt.js +259 -0
- package/src/agent/react-loop.js +697 -0
- package/src/agent/subagent.js +263 -0
- package/src/commands/config.js +53 -0
- package/src/commands/connect.js +190 -0
- package/src/commands/devices.js +121 -0
- package/src/commands/login.js +77 -0
- package/src/commands/logout.js +31 -0
- package/src/commands/mcp.js +258 -0
- package/src/commands/provider.js +633 -0
- package/src/commands/register.js +74 -0
- package/src/commands/run.js +150 -0
- package/src/commands/search.js +64 -0
- package/src/commands/session.js +57 -0
- package/src/commands/skills.js +54 -0
- package/src/commands/stop-subagent.js +58 -0
- package/src/index.js +208 -0
- package/src/llm/direct.js +317 -0
- package/src/memory/store.js +215 -0
- package/src/mock-readline.js +27 -0
- package/src/parser/dependencies.js +71 -0
- package/src/parser/markdown.js +505 -0
- package/src/parser/stream.js +96 -0
- package/src/prompts/modes/CODING.js +160 -0
- package/src/prompts/modes/GENERAL.js +105 -0
- package/src/prompts/modes/NETWORK.js +69 -0
- package/src/prompts/modes/SSH.js +53 -0
- package/src/prompts/systemPrompt.js +85 -0
- package/src/safety/check.js +210 -0
- package/src/services/crypto.js +78 -0
- package/src/services/executor.js +68 -0
- package/src/services/history.js +58 -0
- package/src/services/server-url.js +11 -0
- package/src/services/session.js +194 -0
- package/src/services/ssh.js +176 -0
- package/src/services/websocket.js +112 -0
- package/src/skills/loader.js +231 -0
- package/src/tools/browser.js +434 -0
- package/src/tools/local.js +1254 -0
- package/src/tools/mcp-client.js +209 -0
- package/src/tools/registry.js +132 -0
- package/src/tools/search-providers.js +237 -0
- package/src/tools/ssh.js +74 -0
- package/src/ui/App.js +2031 -0
- package/src/ui/animation.js +47 -0
- package/src/ui/components/AskUserDialog.js +33 -0
- package/src/ui/components/ConfirmationDialog.js +45 -0
- package/src/ui/components/DiffView.js +201 -0
- package/src/ui/components/Header.js +157 -0
- package/src/ui/components/HistoryPicker.js +130 -0
- package/src/ui/components/InputShell.js +22 -0
- package/src/ui/components/MessageHistory.js +1200 -0
- package/src/ui/components/ModalPanel.js +40 -0
- package/src/ui/components/ModePicker.js +161 -0
- package/src/ui/components/PlanDialog.js +48 -0
- package/src/ui/components/ProviderMenu.js +1095 -0
- package/src/ui/components/SavePicker.js +106 -0
- package/src/ui/components/SelectMenu.js +194 -0
- package/src/ui/components/SlashMenu.js +168 -0
- package/src/ui/components/SubagentPanel.js +138 -0
- package/src/ui/components/TextInputSafe.js +117 -0
- package/src/ui/components/TodoPanel.js +54 -0
- package/src/ui/components/ToolExecution.js +261 -0
- package/src/ui/components/TranscriptViewport.js +99 -0
- package/src/ui/diff.js +249 -0
- package/src/ui/h.js +7 -0
- package/src/ui/mouse-scroll.js +63 -0
- package/src/ui/slash-picker.js +58 -0
- package/src/ui/terminal.js +41 -0
- package/src/ui/theme.js +5 -0
- package/src/ui/welcome.js +12 -0
- package/src/utils/constants.js +231 -0
- package/src/utils/helpers.js +154 -0
- package/src/utils/logger.js +81 -0
- 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
|
+
};
|