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,263 @@
1
+ import { TOOLS, MODES, CRITICAL_ENV_EXCLUDE, CRITICAL_ENV_FILENAMES, CRITICAL_ENV_FILENAME_PATTERNS, CRITICAL_ENV_PATH_SEGMENTS, READ_FRESHNESS_INTERACTIONS, MAX_PERSISTED_READ_FILES } from '../../utils/constants.js';
2
+ import { simpleHash } from '../../utils/helpers.js';
3
+ import { resolvePath } from '../../tools/local.js';
4
+ import { memory } from '../../memory/store.js';
5
+ import path from 'path';
6
+ import fs from 'fs';
7
+
8
+ export default {
9
+ _normalizeTrackedPath(filePath) {
10
+ try {
11
+ if (!filePath || typeof filePath !== 'string') return null;
12
+ let raw = filePath.trim();
13
+ raw = raw.replace(/^["'`]+|["'`]+$/g, '');
14
+ const expanded = resolvePath(raw);
15
+ if (!expanded || typeof expanded !== 'string') return null;
16
+ const normalizedSlashes = expanded.replace(/[\\/]+/g, path.sep);
17
+ const absolute = path.isAbsolute(normalizedSlashes)
18
+ ? normalizedSlashes
19
+ : path.resolve(process.cwd(), normalizedSlashes);
20
+ const normalized = path.normalize(absolute);
21
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
22
+ } catch {
23
+ return null;
24
+ }
25
+ },
26
+
27
+ _isCriticalEnvFile(filePath) {
28
+ if (!filePath || typeof filePath !== 'string') return false;
29
+ const normalizedPath = filePath.replace(/[\\/]+/g, '/').toLowerCase();
30
+ const basename = path.basename(normalizedPath).trim();
31
+ if (!basename) return false;
32
+ if (CRITICAL_ENV_EXCLUDE.has(basename)) return false;
33
+ if (CRITICAL_ENV_FILENAMES.has(basename)) return true;
34
+ for (const pattern of CRITICAL_ENV_FILENAME_PATTERNS) {
35
+ if (pattern.test(basename)) return true;
36
+ }
37
+ for (const segment of CRITICAL_ENV_PATH_SEGMENTS) {
38
+ if (normalizedPath.includes(`/${segment}/`) || normalizedPath.startsWith(`${segment}/`) || normalizedPath === segment) return true;
39
+ }
40
+ return false;
41
+ },
42
+
43
+ _isCriticalEnvPattern(value) {
44
+ if (!value || typeof value !== 'string') return false;
45
+ const normalized = value.replace(/[\\/]+/g, '/').toLowerCase();
46
+ const parts = normalized.split('/').filter(Boolean);
47
+ const basename = parts[parts.length - 1] || normalized;
48
+
49
+ if (CRITICAL_ENV_EXCLUDE.has(basename)) return false;
50
+ if (CRITICAL_ENV_FILENAMES.has(basename)) return true;
51
+ for (const pattern of CRITICAL_ENV_FILENAME_PATTERNS) {
52
+ if (pattern.test(basename)) return true;
53
+ }
54
+ for (const segment of CRITICAL_ENV_PATH_SEGMENTS) {
55
+ if (parts.includes(segment.toLowerCase())) return true;
56
+ }
57
+ return false;
58
+ },
59
+
60
+ _commandReferencesCriticalEnvFile(command) {
61
+ if (!command || typeof command !== 'string') return false;
62
+ const readCommandPattern = /\b(cat|type|more|less|head|tail|grep|rg|awk|sed|findstr|get-content|gc)\b/i;
63
+ if (!readCommandPattern.test(command)) return false;
64
+
65
+ const tokens = command
66
+ .match(/"[^"]+"|'[^']+'|`[^`]+`|[^\s;&|<>]+/g)
67
+ ?.map((token) => token.replace(/^["'`]+|["'`]+$/g, '')) || [];
68
+
69
+ return tokens.some((token) => this._isCriticalEnvPattern(token));
70
+ },
71
+
72
+ _subagentCriticalReadBlock(toolCall) {
73
+ if (!this.isSubagent) return null;
74
+ const tool = toolCall.tool || TOOLS.LOCAL_CMD;
75
+ const pathValue = toolCall.path || toolCall.source || '';
76
+
77
+ if ([TOOLS.READ_FILE, TOOLS.LIST_DIR, TOOLS.GET_DEPENDENCIES].includes(tool) && this._isCriticalEnvFile(pathValue)) {
78
+ return `Subagent blocked: "${pathValue}" is a critical environment/secret file and requires direct user confirmation.`;
79
+ }
80
+
81
+ if ([TOOLS.GREP, TOOLS.SEARCH_FILE].includes(tool)) {
82
+ const filePattern = toolCall.filePattern || '';
83
+ const searchesSpecificCriticalPath = pathValue && this._isCriticalEnvFile(pathValue);
84
+ const searchesCriticalPattern = filePattern && this._isCriticalEnvPattern(filePattern);
85
+ const recursiveUnscopedSearch = !filePattern && pathValue && !this._isExistingFileTarget({ path: pathValue });
86
+ if (searchesSpecificCriticalPath || searchesCriticalPattern || recursiveUnscopedSearch) {
87
+ return 'Subagent blocked: content search could read critical environment/secret files. Use non-sensitive file patterns or ask the parent agent for user confirmation.';
88
+ }
89
+ }
90
+
91
+ if (tool === TOOLS.LOCAL_CMD && this._commandReferencesCriticalEnvFile(toolCall.cmd || '')) {
92
+ return 'Subagent blocked: local command appears to read a critical environment/secret file and requires direct user confirmation.';
93
+ }
94
+
95
+ return null;
96
+ },
97
+
98
+ _buildVerificationStateSnapshot() {
99
+ const trackedFiles = {};
100
+ let i = 0;
101
+ for (const [trackedPath, state] of this._readFileState.entries()) {
102
+ trackedFiles[trackedPath] = {
103
+ lastReadInteraction: state.lastReadInteraction,
104
+ lastReadIteration: state.lastReadIteration,
105
+ lastReadAt: state.lastReadAt,
106
+ contentHash: state.contentHash,
107
+ mtimeMs: state.mtimeMs,
108
+ };
109
+ i += 1;
110
+ if (i >= MAX_PERSISTED_READ_FILES) break;
111
+ }
112
+ return {
113
+ freshnessWindow: READ_FRESHNESS_INTERACTIONS,
114
+ interactionIndex: this._toolInteractionCounter,
115
+ filesReadOps: this._readFileOps,
116
+ filesReadUnique: this._readFiles.size,
117
+ blockedWritesWithoutRead: this._blockedWritesWithoutRead,
118
+ trackedFiles,
119
+ updatedAt: new Date().toISOString(),
120
+ };
121
+ },
122
+
123
+ async _persistVerificationState() {
124
+ try {
125
+ await memory.setPreference('verification_state', this._buildVerificationStateSnapshot());
126
+ } catch {}
127
+ },
128
+
129
+ async _loadVerificationState() {
130
+ if (this._verificationStateLoaded) return;
131
+ this._verificationStateLoaded = true;
132
+ try {
133
+ const state = await memory.getPreference('verification_state', null);
134
+ if (!state || typeof state !== 'object') return;
135
+
136
+ const trackedFiles = state.trackedFiles && typeof state.trackedFiles === 'object'
137
+ ? state.trackedFiles
138
+ : {};
139
+
140
+ for (const [trackedPath, fileState] of Object.entries(trackedFiles)) {
141
+ if (!trackedPath || typeof trackedPath !== 'string') continue;
142
+ const normalizedPath = this._normalizeTrackedPath(trackedPath) || trackedPath;
143
+ const lastReadInteraction = Number(fileState?.lastReadInteraction);
144
+ const safeLastReadInteraction = Number.isFinite(lastReadInteraction)
145
+ ? Math.max(0, lastReadInteraction)
146
+ : 0;
147
+ this._readFiles.add(normalizedPath);
148
+ this._readFileState.set(normalizedPath, {
149
+ lastReadInteraction: safeLastReadInteraction,
150
+ lastReadIteration: Number(fileState?.lastReadIteration) || 0,
151
+ lastReadAt: fileState?.lastReadAt || null,
152
+ contentHash: fileState?.contentHash || null,
153
+ mtimeMs: fileState?.mtimeMs ?? null,
154
+ });
155
+ }
156
+
157
+ const persistedCounter = Number(state.interactionIndex);
158
+ if (Number.isFinite(persistedCounter)) {
159
+ this._toolInteractionCounter = Math.max(this._toolInteractionCounter, Math.max(0, persistedCounter));
160
+ }
161
+ const persistedReadOps = Number(state.filesReadOps);
162
+ if (Number.isFinite(persistedReadOps)) {
163
+ this._readFileOps = Math.max(this._readFileOps, Math.max(0, persistedReadOps));
164
+ }
165
+ const persistedBlocked = Number(state.blockedWritesWithoutRead);
166
+ if (Number.isFinite(persistedBlocked)) {
167
+ this._blockedWritesWithoutRead = Math.max(this._blockedWritesWithoutRead, Math.max(0, persistedBlocked));
168
+ }
169
+ } catch {}
170
+ },
171
+
172
+ async _trackReadFile(toolCall, result) {
173
+ const tool = toolCall.tool || TOOLS.LOCAL_CMD;
174
+ if (tool !== TOOLS.READ_FILE || !result?.success) return;
175
+ const trackedPath = this._normalizeTrackedPath(toolCall.path);
176
+ if (trackedPath) {
177
+ this._readFiles.add(trackedPath);
178
+ if (!toolCall.startLine && !toolCall.endLine) {
179
+ this._sessionReadFiles.add(trackedPath);
180
+ }
181
+ const rawContent = String(result.output || '');
182
+ let mtimeMs = null;
183
+ try { mtimeMs = fs.statSync(trackedPath).mtimeMs; } catch {}
184
+ this._readFileState.set(trackedPath, {
185
+ lastReadInteraction: this._toolInteractionCounter,
186
+ lastReadIteration: this.iteration,
187
+ lastReadAt: new Date().toISOString(),
188
+ contentHash: simpleHash(rawContent),
189
+ mtimeMs,
190
+ });
191
+ }
192
+ this._readFileOps += 1;
193
+ await this._persistVerificationState();
194
+ },
195
+
196
+ async _trackWriteFile(toolCall, result) {
197
+ const tool = toolCall.tool || TOOLS.LOCAL_CMD;
198
+ if (!result?.success) return;
199
+ const writeTools = [TOOLS.EDIT_FILE, TOOLS.WRITE_FILE, TOOLS.APPEND_FILE];
200
+ if (!writeTools.includes(tool)) return;
201
+ const trackedPath = this._normalizeTrackedPath(toolCall.path);
202
+ if (!trackedPath) return;
203
+ try {
204
+ const stat = fs.statSync(trackedPath);
205
+ this._readFiles.add(trackedPath);
206
+ const existing = this._readFileState.get(trackedPath) || {};
207
+ this._readFileState.set(trackedPath, {
208
+ ...existing,
209
+ mtimeMs: stat.mtimeMs,
210
+ lastReadInteraction: this._toolInteractionCounter,
211
+ });
212
+ } catch {}
213
+ },
214
+
215
+ _requiresReadBeforeModify(toolCall) {
216
+ const tool = toolCall.tool || TOOLS.LOCAL_CMD;
217
+ return tool === TOOLS.EDIT_FILE || tool === TOOLS.APPEND_FILE || tool === TOOLS.WRITE_FILE;
218
+ },
219
+
220
+ _isFirstReadWithRange(toolCall) {
221
+ const tool = toolCall.tool || TOOLS.LOCAL_CMD;
222
+ if (this.mode !== MODES.CODING) return false;
223
+ if (tool !== TOOLS.READ_FILE) return false;
224
+ const hasRange = toolCall.startLine != null || toolCall.endLine != null;
225
+ if (!hasRange) return false;
226
+ const trackedPath = this._normalizeTrackedPath(toolCall.path);
227
+ if (!trackedPath) return false;
228
+ return !this._readFiles.has(trackedPath);
229
+ },
230
+
231
+ _isExistingFileTarget(toolCall) {
232
+ const trackedPath = this._normalizeTrackedPath(toolCall.path);
233
+ if (!trackedPath) return false;
234
+ try {
235
+ const stat = fs.statSync(trackedPath);
236
+ return stat.isFile();
237
+ } catch {
238
+ return false;
239
+ }
240
+ },
241
+
242
+ _getReadFreshnessForTarget(toolCall) {
243
+ const trackedPath = this._normalizeTrackedPath(toolCall.path);
244
+ if (!trackedPath) return { ok: false, reason: 'invalid_target', trackedPath: null, age: null };
245
+ const state = this._readFileState.get(trackedPath);
246
+ if (!state) return { ok: false, reason: 'not_read', trackedPath, age: null };
247
+ if (this._lastCompactInteraction != null && state.lastReadInteraction < this._lastCompactInteraction) {
248
+ return { ok: false, reason: 'stale_read', trackedPath, age: this._toolInteractionCounter - state.lastReadInteraction };
249
+ }
250
+ if (state.mtimeMs != null) {
251
+ try {
252
+ const stat = fs.statSync(trackedPath);
253
+ if (stat.mtimeMs !== state.mtimeMs) {
254
+ const age = this._toolInteractionCounter - (state.lastReadInteraction || 0);
255
+ return { ok: false, reason: 'stale_read', trackedPath, age };
256
+ }
257
+ } catch {
258
+ return { ok: false, reason: 'stale_read', trackedPath, age: null };
259
+ }
260
+ }
261
+ return { ok: true, reason: 'fresh', trackedPath, age: 0 };
262
+ },
263
+ };
@@ -0,0 +1,80 @@
1
+ import WebSocket from 'ws';
2
+ import { DEFAULTS } from '../../utils/constants.js';
3
+ import { logger } from '../../utils/logger.js';
4
+ import { sleep } from '../../utils/helpers.js';
5
+
6
+ export default {
7
+ async _connectWebSocket() {
8
+ if (this.ws) {
9
+ try {
10
+ this.ws.removeAllListeners();
11
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
12
+ this.ws.close();
13
+ }
14
+ } catch {}
15
+ this.ws = null;
16
+ }
17
+
18
+ const wsUrl = this.server.replace('http://', 'ws://').replace('https://', 'wss://');
19
+ let lastError = null;
20
+ const maxAttempts = DEFAULTS.WS_RECONNECT_MAX_ATTEMPTS;
21
+
22
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
23
+ if (this._cancelled) throw new Error('Agent cancelled during WebSocket connection');
24
+
25
+ try {
26
+ const result = await new Promise((resolve, reject) => {
27
+ try {
28
+ const ws = new WebSocket(`${wsUrl}/ws`, { handshakeTimeout: 10000 });
29
+ const connectTimeout = setTimeout(() => { ws.terminate(); reject(new Error(`WebSocket connection timeout (${10}s)`)); }, 10000);
30
+
31
+ ws.on('open', () => {
32
+ this.onConnectionChange(true);
33
+ ws.send(JSON.stringify({ type: 'REGISTER', token: this.token }));
34
+ logger.debug('WebSocket connected to server');
35
+ });
36
+ ws.on('message', async (data) => {
37
+ try {
38
+ const msg = JSON.parse(data.toString());
39
+ if (msg.type === 'REGISTERED') { clearTimeout(connectTimeout); this.ws = ws; resolve(msg); }
40
+ else if (msg.type === 'COMMANDS') { this.pendingCommands = msg.commands; this.currentTaskId = msg.taskId; }
41
+ else if (msg.type === 'CANCEL_ACK') { logger.info('Cancellation acknowledged by server'); }
42
+ else if (msg.type === 'ERROR') { logger.warn('WebSocket error message', { message: msg.message }); }
43
+ else if (msg.type === 'CANCEL_SUBAGENT') {
44
+ if (this._subagentActive && this._subagentLoop) {
45
+ this._subagentLoop.cancel();
46
+ // Optional: send acknowledgment back to server
47
+ // this.ws.send(JSON.stringify({ type: 'CANCEL_SUBAGENT_ACK' }));
48
+ }
49
+ }
50
+ } catch (parseErr) { logger.debug('Invalid WebSocket message', { error: parseErr.message }); }
51
+ });
52
+ ws.on('error', (err) => {
53
+ clearTimeout(connectTimeout);
54
+ this.onConnectionChange(false);
55
+ logger.error('WebSocket error', { error: err.message });
56
+ reject(err);
57
+ });
58
+ ws.on('close', (code) => {
59
+ clearTimeout(connectTimeout);
60
+ this.onConnectionChange(false);
61
+ logger.debug('WebSocket closed', { code });
62
+ this.ws = null;
63
+ });
64
+ ws.on('ping', () => { try { ws.pong(); } catch {} });
65
+ } catch (err) { reject(err); }
66
+ });
67
+ return result;
68
+ } catch (err) {
69
+ lastError = err;
70
+ if (attempt < maxAttempts - 1) {
71
+ const delay = DEFAULTS.WS_RECONNECT_BASE_DELAY * Math.pow(2, attempt);
72
+ logger.warn(`WebSocket connection failed, retrying in ${delay}ms`, { attempt: attempt + 1, error: err.message });
73
+ await sleep(delay);
74
+ }
75
+ }
76
+ }
77
+ this.onConnectionChange(false);
78
+ throw new Error(`WebSocket connection failed after ${maxAttempts} attempts: ${lastError?.message}`);
79
+ },
80
+ };
@@ -0,0 +1,259 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Agent Prompt Builder (v4.0 — Coding Mode + Anti-Hallucination)
3
+ // =============================================================================
4
+
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import os from 'os';
8
+ import { detectOS, getSystemInfo } from '../tools/local.js';
9
+ import { APP_NAME, TOOLS, MODES, EXECUTION_MODES } from '../utils/constants.js';
10
+ import { logger } from '../utils/logger.js';
11
+
12
+ const loadCustomInstructions = async () => {
13
+ const paths = [
14
+ path.join(os.homedir(), '.osai-agent', 'instructions.md'),
15
+ path.join(os.homedir(), '.osai-agent', 'instructions.m'),
16
+ path.join(os.homedir(), '.osai-agent', 'instructions.txt'),
17
+ path.join(process.cwd(), '.osai-agent-instructions.md'),
18
+ path.join(process.cwd(), '.osai-agent-instructions.txt'),
19
+ ];
20
+ for (const p of paths) {
21
+ try {
22
+ const content = await fs.readFile(p, 'utf-8');
23
+ if (content.trim()) { logger.debug('Loaded custom instructions', { path: p }); return content.trim(); }
24
+ } catch {}
25
+ }
26
+ return '';
27
+ };
28
+
29
+ export const buildSystemPrompt = async (osName = 'windows', mode = 'GENERAL', executionMode = 'EXEC') => {
30
+ const osLabel = osName.charAt(0).toUpperCase() + osName.slice(1);
31
+ const systemInfo = getSystemInfo();
32
+ const customInstructions = await loadCustomInstructions();
33
+ const isCoding = mode === MODES.CODING;
34
+ const isPlan = executionMode === EXECUTION_MODES.PLAN;
35
+
36
+ const modeSection = isCoding ? `
37
+ ## MODE: CODING
38
+
39
+ You are in CODING MODE. This means:
40
+ - You are a senior software engineer with full coding capabilities.
41
+ - File modifications (WRITE_FILE, EDIT_FILE, APPEND_FILE, DELETE_FILE, MOVE_FILE, COPY_FILE, RUN_SCRIPT) are auto-approved.
42
+ - Use TODO_ADD to plan your tasks before starting. Update todos as you progress with TODO_UPDATE.
43
+ - Use TODO_COMPLETE when a task is done. Always check TODO_LIST before asking what to do next.
44
+ - Prefer EDIT_FILE for small changes and WRITE_FILE for creating new files.
45
+ - Use SEARCH_FILE to find code patterns, variable names, or functions across files.
46
+ - Use TREE_VIEW to understand project structure before making changes.
47
+ - Use RUN_SCRIPT to execute test suites, linters, or build commands.
48
+ - Use FETCH_URL to read documentation, API specs, or reference material from the web.
49
+ - Use WEB_SEARCH to search for solutions, error messages, or documentation online.
50
+ - Always verify your changes work by running tests or the build when available.
51
+ ` : mode === MODES.NETWORK ? `
52
+ ## MODE: NETWORK
53
+
54
+ You are in NETWORK MODE. You manage network devices via SSH.
55
+ - Use SSH_CMD to execute commands on remote network devices.
56
+ - Focus on network configuration, monitoring, and troubleshooting.
57
+ ` : `
58
+ ## MODE: GENERAL
59
+
60
+ You are a system administration agent with broad capabilities.
61
+ - Manage files, execute commands, and maintain the system.
62
+ - Use web tools to fetch documentation and search for solutions.
63
+ - Generate PDF (WeasyPrint), Word (python-docx), PowerPoint (python-pptx), and Excel (openpyxl) documents.
64
+ Workflow: check Python (${osName === 'windows' ? 'python --version' : 'python3 --version'}) →
65
+ install if missing → create output/ dir → set up a Python virtual environment (venv) →
66
+ install libraries in the venv → write a Python script with a descriptive filename →
67
+ run it via the venv's Python → verify → report the absolute path to the user.
68
+ `;
69
+
70
+ const execModeSection = isPlan ? `
71
+ + You are in PLAN mode — only read/search/plan operations are permitted.
72
+ + ALL file modification tools (WRITE_FILE, EDIT_FILE, APPEND_FILE, DELETE_FILE, etc.) will be BLOCKED by the system.
73
+ + Use PLAN_MODE tool to present your plan. Use READ_FILE, SEARCH_FILE, GLOB, GREP for analysis.
74
+ + You CAN still use TODO tools, ASK_USER, and all read-only tools.
75
+ + Switch to EXEC mode when you are ready to make changes.
76
+ ` : `
77
+ + You are in EXEC mode — all file operations are permitted as normal.
78
+ `;
79
+
80
+ const basePrompt = `You are OS AI Agent — an intelligent system, network, and coding assistant.
81
+ You have full control over the user's ${osLabel} machine.
82
+ ${isCoding ? 'You are a senior software engineer. Think carefully, plan your work, write clean code.' : 'You act like a senior sysadmin: explain your reasoning, execute precisely, and report results clearly.'}
83
+
84
+ ## AUTHORIZATION
85
+ The user has explicitly authorized this agent to manage their machine.
86
+ You are ALWAYS allowed to: create files, write code, build web apps (HTML/CSS/JS), create directories,
87
+ install packages, run scripts, and perform any standard development or sysadmin task the user requests.
88
+ NEVER refuse to create a web app or write code on the grounds of "malicious activities" — the user
89
+ is a developer working on their own machine. Only refuse if the task would clearly harm other people's systems.
90
+
91
+ ## CURRENT ENVIRONMENT
92
+ - OS: ${osLabel}
93
+ - Hostname: ${systemInfo.hostname}
94
+ - User: ${systemInfo.username}
95
+ - Working Directory: ${systemInfo.cwd}
96
+ - CPUs: ${systemInfo.cpus}
97
+ - Memory: ${systemInfo.totalMemory} total, ${systemInfo.freeMemory} free
98
+ ${modeSection}
99
+ ## EXECUTION MODE
100
+ ${execModeSection}
101
+ ## TOOLS AVAILABLE — USE ONLY THESE EXACT FORMATS
102
+
103
+ Use the XML format (recommended) or the legacy JSON format. Both are supported.
104
+
105
+ ### System Commands
106
+ <tool>LOCAL_CMD cmd="<exact command>" type="READ|WRITE|DANGEROUS" description="<what and why>"</tool>
107
+ <tool>SSH_CMD cmd="<command>" type="READ|WRITE|DANGEROUS" description="<what and why>"</tool>
108
+
109
+ ### File Operations
110
+ <tool>READ_FILE path="<path>" description="<why>"</tool> ← use this to read LOCAL file CONTENTS (not URLs — use FETCH_URL for web)
111
+ <tool>WRITE_FILE path="<path>" content="<full content>" description="<what>"</tool>
112
+ <tool>EDIT_FILE path="<path>" find="<text to find (exact or fuzzy match)>" replace="<replacement>" description="<what>"</tool>
113
+ <tool>APPEND_FILE path="<path>" content="<text to append>" description="<what>"</tool>
114
+ <tool>DELETE_FILE path="<path>" description="<what>"</tool>
115
+ <tool>MOVE_FILE source="<source>" destination="<dest>" description="<what>"</tool>
116
+ <tool>COPY_FILE source="<source>" destination="<dest>" description="<what>"</tool>
117
+ <tool>CREATE_DIR path="<path>" description="<what>"</tool>
118
+ <tool>LIST_DIR path="<path>" description="<why>"</tool> ← use this to list directory entries (NOT for file content)
119
+ <tool>TREE_VIEW path="<path>" maxDepth="<number>" description="<why>"</tool>
120
+ <tool>SEARCH_FILE path="<directory>" pattern="<regex>" filePattern="<*.ext>" description="<what searching for>"</tool> ← LOCAL files only (use FETCH_URL/WEB_SEARCH for web content)
121
+ <tool>FILE_INFO path="<path>" description="<why>"</tool>
122
+
123
+ ### Script Execution
124
+ <tool>RUN_SCRIPT path="<script path>" args="<arguments>" interpreter="<optional>" description="<what>"</tool>
125
+
126
+ ### Web Tools
127
+ <tool>FETCH_URL url="<https://...>" description="<what you want from this URL>"</tool>
128
+ <tool>WEB_SEARCH query="<search query>" description="<why you need this info>"</tool>
129
+
130
+ ### Browser Tools (Playwright headless Chromium — reads JS-rendered pages)
131
+ <tool>BROWSE url="<https://...>" description="<what you want to read on this page>"</tool>
132
+ <tool>BROWSE_SEARCH query="<search query>" description="<what you are searching for>"</tool>
133
+ <tool>BROWSE_EXTRACT url="<https://...>" selectors={"key":"CSS selector"} description="<what data you need>"</tool>
134
+
135
+ ### Todo Management
136
+ <tool>TODO_ADD text="<task description>" priority="<HIGH|MEDIUM|LOW>"</tool>
137
+ <tool>TODO_COMPLETE id="<todo number>"</tool>
138
+ <tool>TODO_UPDATE id="<todo number>" text="<new text>" priority="<HIGH|MEDIUM|LOW>"</tool>
139
+ <tool>TODO_LIST</tool>
140
+ <tool>TODO_CLEAR</tool>
141
+
142
+ ### MCP (Model Context Protocol)
143
+ <tool>MCP_TOOL server="<server>" mcpTool="<tool>" params={...}</tool>
144
+ Use this to call tools from external MCP servers configured via "osai-agent mcp add".
145
+
146
+ ## CRITICAL RULES
147
+
148
+ 1. ONLY use tools listed above. NEVER invent a tool name that does not exist.
149
+ 2. NEVER output tool calls for tools like "BROWSE_WEB", "INSTALL_PACKAGE", "GIT_CLONE", "COMPILE", etc. Use the correct tools instead.
150
+ 3. ALWAYS output tool calls on their own line. Use the XML format: <tool>NAME key="value"</tool> (recommended). The legacy JSON format {"tool":"NAME","key":"value"} is also supported. NEVER wrap tool calls in backticks, code fences, or markdown.
151
+ 4. ONE tool call per turn. Wait for [TOOL_RESULT] before proceeding.
152
+ 5. Exception to rule 4: you MAY output MULTIPLE READ_FILE tool calls in a single turn when you need to read several files at once (e.g. when asked to "read all files" or "check all files"). Each READ_FILE must be on its own line. After ALL results come back, analyze everything together. Do NOT mix READ_FILE with other tools in the same turn.
153
+ 6. NEVER simulate or fabricate command output. Only interpret real [TOOL_RESULT] data.
154
+ 7. For installing packages: use LOCAL_CMD with "npm install", "pip install", "apt install", etc.
155
+ 8. For cloning repos: use LOCAL_CMD with "git clone <url>".
156
+ 9. For web browsing: use FETCH_URL with a specific URL, or WEB_SEARCH for search queries.
157
+ 10. GREP, READ_FILE, SEARCH_FILE, GLOB work on LOCAL FILES ONLY — they cannot read web URLs or parse HTML. Use FETCH_URL or WEB_SEARCH for web content.
158
+ 11. NEVER use a tool to answer questions about yourself or your capabilities.
159
+ 12. If you are unsure which tool to use, use LOCAL_CMD to run a shell command.
160
+ 13. Respond in the same language as the user.
161
+ 13b. Use first-person ("I"/"me") for yourself. Refer to the human as "you" (second person), never as "the user".
162
+ 14. Never ask what OS the user has — it is: ${osLabel}.
163
+ 15. If a command fails, analyze the error and propose a corrected approach.
164
+ 16. Prefer file tools over LOCAL_CMD for file operations.
165
+ 17. Keep explanations concise but informative.
166
+ 18. BEFORE the first modification of an existing file in the current context, call READ_FILE on that same path.
167
+ 19. BEFORE modifying a file, ensure all its local dependencies (imports, linked CSS/JS) have been read. Use GET_DEPENDENCIES to discover what a file's local imports export WITHOUT reading their full contents. If you need the actual implementation, use READ_FILE on the specific dependency.
168
+ 20. Do NOT re-read the same file before every edit. Reuse the last READ_FILE for that path while it is fresh (<= 20 interactions). Re-read only if older than 20 interactions or if context is uncertain.
169
+ 21. In CODING mode, always treat the current working directory as the default project root and include it in your planning.
170
+ 22. In CODING mode, before operating outside the current working directory (or changing directory), you must request user approval first.
171
+ ${isCoding ? `23. Always create a todo plan with TODO_ADD before starting multi-step tasks.
172
+ 24. Always verify your changes by running tests or builds when available.
173
+ 25. If a tool fails with ENOENT, "not recognized", or "spawn error", the binary is missing — output [BLOCKED] immediately, do NOT retry.
174
+ 26. READ_FILE without startLine/endLine reads the ENTIRE file at once. NEVER paginate a file by reading it in chunks (startLine 1-20, then 21-40, etc.) — this is forbidden and wastes iterations. Read once, reason on the full content.
175
+ 27. NEVER call READ_FILE on the same path more than once unless you have explicitly modified that file since the last read. The content does not change between reads. If you already have the content in context, USE IT — do not re-read.
176
+ 28. After reading a file and finding no issues requiring line-by-line inspection, make your edits directly with EDIT_FILE or WRITE_FILE. Do not loop back to read again.
177
+ 29. If you search (GREP/SEARCH_FILE/GLOB) 3+ times without making any edits, you are in a loop. Stop, change strategy, and output [BLOCKED] explaining why your approach failed.
178
+ 30. Before searching, ask yourself: what exact file or pattern am I looking for? Do NOT search for vague patterns hoping to find something useful — this wastes iterations.
179
+ 31. If you find yourself cycling through READ → EDIT → DIAG without making real progress, you are in a loop. Stop, re-assess the problem, and output [BLOCKED] describing why your approach failed.
180
+ 32. When you change the working directory (cd command), a visual animation will appear to indicate the directory change. In CODING mode, a y/n confirmation prompt will also appear before the change takes effect.
181
+ 33. GET_DEPENDENCIES resolves a file's local imports and returns a compact summary of each dependency's exported symbols, file size, and sub-imports — WITHOUT reading the full file. Use it after READ_FILE to decide whether you need to read a dependency in full. It is much faster than calling READ_FILE on every imported file.
182
+ 34. Use thinking/reflection for planning and reasoning only. Do NOT emit tool calls while still in a reasoning/thinking phase. Complete your analysis first, then emit the tool call. Never intermix tool calls with ongoing reasoning.` : `23. If a tool fails repeatedly with the same error, output [BLOCKED] immediately — do not retry in a loop.
183
+ 24. READ_FILE without startLine/endLine reads the ENTIRE file. Never paginate by reading chunks of lines. Read once, use the result.
184
+ 25. Never re-read a file you already have in context unless you modified it.
185
+ 26. Use thinking/reflection for planning and reasoning only. Do NOT emit tool calls while still in a reasoning/thinking phase. Complete your analysis first, then emit the tool call. Never intermix tool calls with ongoing reasoning.`}
186
+
187
+ ## THINKING vs TOOL EXECUTION
188
+
189
+ Thinking and reflection are for planning and analysis only. Do NOT emit tool calls while still in a reasoning/thinking phase. Follow this pattern:
190
+
191
+ 1. Think/reason about the problem and plan your approach.
192
+ 2. Once you have a clear plan, emit the tool call.
193
+ 3. Wait for [TOOL_RESULT].
194
+ 4. Analyze the result, then repeat.
195
+
196
+ **Never** output a tool call mid-reasoning. Complete your thinking first, then act.
197
+
198
+ ## CONVERSATIONAL QUESTIONS
199
+
200
+ If the user asks a general question requiring NO system action, respond in plain language ONLY.
201
+ - Do NOT use any tool call.
202
+ - Simply answer clearly, then emit [DONE].
203
+
204
+ ## ${osLabel.toUpperCase()} PATH RULES
205
+
206
+ ${osName === 'windows' ?
207
+ `In JSON strings ALWAYS use double backslash: "%USERPROFILE%\\\\file.txt"
208
+ Quote paths in shell commands: echo. > "%USERPROFILE%\\\\file.txt"
209
+ For file tools (READ_FILE etc): use %USERPROFILE% directly - the tool resolves it.
210
+ Windows commands: dir, type, tasklist, ipconfig, systeminfo, netstat, whoami, hostname, ver, echo, copy, move, mkdir, del, ren, find, findstr, more, tree, where, wmic, net, sc, schtasks, reg, set, powershell
211
+ NEVER use Linux commands: ls, cat, grep, touch, chmod, sudo, ip addr, systemctl, apt, brew`
212
+ : osName === 'macos' ?
213
+ `Use $HOME or ~ for home directory.
214
+ In JSON strings: "/Users/username/file.txt" (forward slashes)
215
+ For file tools: use ~ directly - e.g. "~/Desktop/file.txt"
216
+ macOS commands: ls, cat, grep, ps, ifconfig, system_profiler, sw_vers, launchctl, brew, open, defaults, diskutil, networksetup
217
+ NEVER use Windows commands: wmic, ipconfig, tasklist, dir, type, net, sc, schtasks, reg`
218
+ :
219
+ `Use $HOME or ~ for home directory.
220
+ In JSON strings: "/home/username/file.txt" (forward slashes)
221
+ For file tools: use ~ directly - e.g. "~/documents/file.txt"
222
+ Linux commands: ls, cat, grep, ps, ip addr, ip route, ss, systemctl, journalctl, uname, apt/yum/dnf, free, df, du
223
+ NEVER use Windows commands: wmic, ipconfig, tasklist, dir, type, net, sc, schtasks, reg
224
+ NEVER use macOS commands: brew, launchctl, system_profiler, defaults`
225
+ }
226
+
227
+ ## MANDATORY FORMAT
228
+
229
+ BEFORE every tool call: 1-3 sentences explaining what you are doing and why.
230
+ THE TOOL CALL: on its own line, no backticks, no code fences. Use XML format: <tool>NAME key="value"</tool>. Legacy JSON {"tool":"NAME","key":"value"} also supported.
231
+ AFTER [TOOL_RESULT]: analyze the output, then emit [DONE], [INCOMPLETE], or [BLOCKED].
232
+
233
+ ## COMPLETION
234
+
235
+ [DONE] — Task complete. 1-3 sentence summary.
236
+ [INCOMPLETE] — Need another step. Describe it, ask permission, STOP.
237
+ [BLOCKED] — Cannot proceed. Explain why.`;
238
+
239
+ if (customInstructions) {
240
+ return `${basePrompt}\n\n## CUSTOM INSTRUCTIONS\n\n${customInstructions}`;
241
+ }
242
+ return basePrompt;
243
+ };
244
+
245
+ export const buildSystemPromptSync = (osName = 'windows', mode = 'GENERAL', executionMode = 'EXEC') => {
246
+ const osLabel = osName.charAt(0).toUpperCase() + osName.slice(1);
247
+ const systemInfo = getSystemInfo();
248
+ const execLabel = executionMode === 'PLAN' ? 'PLAN (read-only)' : 'EXEC';
249
+ return `You are OS AI Agent — an intelligent system, coding, and network assistant.
250
+ OS: ${osLabel} | Hostname: ${systemInfo.hostname} | CWD: ${systemInfo.cwd}
251
+
252
+ TOOLS: LOCAL_CMD, READ_FILE, WRITE_FILE, EDIT_FILE, LIST_DIR, SEARCH_FILE, APPEND_FILE, DELETE_FILE, CREATE_DIR, TREE_VIEW, RUN_SCRIPT, MOVE_FILE, COPY_FILE, FILE_INFO, FETCH_URL, WEB_SEARCH, TODO_ADD, TODO_COMPLETE, TODO_UPDATE, TODO_LIST, TODO_CLEAR
253
+ MODE: ${mode}/${execLabel}
254
+
255
+ FORMAT: Explain -> XML tool call -> Analyze [TOOL_RESULT] -> [DONE]/[INCOMPLETE]/[BLOCKED]
256
+ RULES: One tool per turn (exception: multiple READ_FILE allowed in parallel). Same language as user. Never simulate output. Never invent tools. Fix errors. Read each file + its dependencies once before modification. Keep work in current cwd unless user approves directory change/out-of-cwd actions. Anti-loop: READ→EDIT→DIAG cycles are automatically detected and blocked.`;
257
+ };
258
+
259
+ export const SYSTEM_PROMPT = buildSystemPromptSync('windows');