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,210 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Safety Check System (v4.0 — Coding Mode aware)
3
+ // =============================================================================
4
+ // Four-tier safety classification: READ, WRITE, DANGEROUS, ASK
5
+ // In CODING mode, WRITE tools auto-approved with --no-confirm.
6
+ // =============================================================================
7
+
8
+ const TIER = {
9
+ READ: 'READ',
10
+ WRITE: 'WRITE',
11
+ DANGEROUS: 'DANGEROUS',
12
+ ASK: 'ASK',
13
+ };
14
+
15
+ const READ_PATTERNS = [
16
+ /^show\s+/i, /^display\s+/i, /^get\s+/i, /^list\s+/i,
17
+ /^ls\s/i, /^cat\s/i, /^grep\s/i, /^ping\s/i,
18
+ /^traceroute\s/i, /^nslookup\s/i, /^dig\s/i, /^whois\s/i,
19
+ /^ip\s+route\s/i, /^ip\s+addr\s/i, /^ifconfig\s/i,
20
+ /^netstat\s/i, /^ss\s/i,
21
+ /^systemctl\s+status\s/i, /^systemctl\s+is-active\s/i, /^systemctl\s+list/i,
22
+ /^docker\s+ps/i, /^docker\s+inspect/i, /^docker\s+logs/i,
23
+ /^kubectl\s+get/i, /^kubectl\s+describe/i, /^kubectl\s+logs/i,
24
+ /^top\s/i, /^htop\s/i, /^ps\s/i, /^df\s/i, /^free\s/i,
25
+ /^uname\s/i, /^hostname\s/i, /^whoami\s/i, /^date\s/i, /^uptime\s/i,
26
+ /^version\s/i, /^running-config/i, /^startup-config/i,
27
+ /^pwd$/i, /^echo\s/i, /^head\s/i, /^tail\s/i, /^wc\s/i,
28
+ /^file\s/i, /^stat\s/i, /^du\s/i, /^lsof\s/i,
29
+ /^ipconfig/i, /^systeminfo/i, /^tasklist/i, /^wmic\s/i,
30
+ /^Get-Process/i, /^Get-Service/i, /^Get-ChildItem/i, /^Get-Content/i,
31
+ /^Get-NetAdapter/i, /^Get-NetIPAddress/i, /^Get-NetRoute/i,
32
+ /^Get-EventLog/i, /^Get-WmiObject/i, /^Get-CimInstance/i,
33
+ /^Get-Disk/i, /^Get-Volume/i, /^Get-ComputerInfo/i,
34
+ /^Test-Connection/i, /^Test-NetConnection/i, /^Resolve-DnsName/i,
35
+ /^dir\b/i, /^type\s/i, /^where\s/i, /^findstr\s/i,
36
+ /^chcp/i, /^ver$/i, /^set$/i, /^driverquery/i, /^qwinsta/i, /^quser/i,
37
+ /^which\s/i, /^env\b/i, /^printenv/i, /^npm\s+list/i, /^npm\s+ls/i,
38
+ /^pip\s+list/i, /^pip\s+show/i, /^node\s+-v/i, /^node\s+--version/i,
39
+ /^python\s+-[Vc]/i, /^python3\s+-[Vc]/i, /^git\s+status/i, /^git\s+log/i,
40
+ /^git\s+diff/i, /^git\s+branch/i, /^git\s+remote/i, /^git\s+show/i,
41
+ ];
42
+
43
+ const WRITE_PATTERNS = [
44
+ /^configure\s+terminal/i, /^conf\s+t/i, /^interface\s+/i, /^vlan\s+/i,
45
+ /^router\s+/i, /^access-list\s+/i, /^nat\s+/i, /^route\s+/i,
46
+ /^ip\s+route\s+add/i, /^ip\s+route\s+change/i, /^ip\s+route\s+replace/i,
47
+ /^systemctl\s+start\s/i, /^systemctl\s+stop\s/i, /^systemctl\s+restart\s/i,
48
+ /^systemctl\s+enable\s/i, /^systemctl\s+disable\s/i,
49
+ /^service\s+start/i, /^service\s+stop/i, /^service\s+restart/i,
50
+ /^iptables\s+/i, /^nft\s+/i, /^ufw\s+/i, /^firewall-cmd\s+/i,
51
+ /^nmcli\s+/i, /^ip\s+link\s+set/i, /^ip\s+addr\s+add/i, /^ip\s+addr\s+del/i,
52
+ /^echo\s+.*>\s*\//i, /^tee\s+/i, /^sed\s+-i/i, /^awk\s+/i,
53
+ /^mkdir\s/i, /^touch\s/i, /^chmod\s/i, /^chown\s/i, /^ln\s+-s/i,
54
+ /^cp\s/i, /^mv\s/i,
55
+ /^git\s+commit/i, /^git\s+push/i, /^git\s+checkout/i, /^git\s+merge/i,
56
+ /^git\s+reset/i, /^git\s+rebase/i,
57
+ /^docker\s+run/i, /^docker\s+exec/i, /^docker\s+build/i,
58
+ /^docker\s+compose/i, /^docker\s+swarm/i,
59
+ /^kubectl\s+apply/i, /^kubectl\s+create/i, /^kubectl\s+delete\s+pod/i,
60
+ /^kubectl\s+delete\s+deployment/i, /^kubectl\s+scale/i, /^kubectl\s+rollout/i,
61
+ /^write\s+memory/i, /^copy\s+running-config\s+startup-config/i, /^wr\s/i,
62
+ /^pip\s+install/i, /^npm\s+install/i, /^npm\s+run/i,
63
+ /^apt(\s+|-get\s+)install/i, /^yum\s+install/i, /^dnf\s+install/i, /^brew\s+install/i,
64
+ /^New-Item/i, /^Set-Content/i, /^Add-Content/i, /^Out-File/i,
65
+ /^Set-Service/i, /^Start-Service/i, /^Stop-Service/i, /^Restart-Service/i,
66
+ /^Enable-NetAdapter/i, /^Disable-NetAdapter/i,
67
+ /^Set-NetIPAddress/i, /^New-NetIPAddress/i,
68
+ /^Set-NetFirewallRule/i, /^New-NetFirewallRule/i,
69
+ /^reg\s+add/i, /^schtasks\s+\/create/i,
70
+ /^net\s+start/i, /^net\s+stop/i, /^net\s+user/i, /^netsh\s/i,
71
+ /^sc\s+config/i, /^copy\s/i, /^move\s/i, /^rename\s/i, /^ren\s/i, /^attrib\s/i,
72
+ ];
73
+
74
+ const DANGEROUS_PATTERNS = [
75
+ /^reload\s/i, /^reboot\s/i, /^shutdown\s/i, /^poweroff\s/i, /^halt\s/i,
76
+ /^init\s+[06]/i,
77
+ /^rm\s+-rf\s+\//i, /^rm\s+-rf\s+\/home/i, /^rm\s+-rf\s+\/usr/i,
78
+ /^rm\s+-rf\s+\/var/i, /^rm\s+-rf\s+\/etc/i, /^rm\s+-rf\s+\/opt/i,
79
+ /^rm\s+-rf\s+\/srv/i, /^rm\s+-rf\s+\.\//i, /^rm\s+-rf\s+\.\./i,
80
+ /^dd\s+if=\/dev\/zero/i, /^dd\s+if=\/dev\/urandom/i,
81
+ />\s*\/dev\/sda/i, />\s*\/dev\/sd[b-z]/i, />\s*\/dev\/nvme/i,
82
+ />\s*\/dev\/vda/i, />\s*\/dev\/vdb/i,
83
+ /^mkfs\./i, /^format\s+c:/i, /^del\s+\/[fq]/i, /^erase\s+/i, /^delete\s+/i,
84
+ /^no\s+ip\s+route/i, /^no\s+interface/i, /^no\s+vlan/i,
85
+ /^no\s+router/i, /^no\s+access-list/i, /^no\s+nat/i,
86
+ /^default\s+interface/i, /^default\s+vlan/i,
87
+ /^clear\s+arp-cache/i, /^clear\s+ip\s+route/i,
88
+ /^clear\s+mac\s+address-table/i, /^clear\s+spanning-tree/i,
89
+ /^clear\s+vlan/i, /^clear\s+interface/i,
90
+ /:\(\)\{\s*:\|:&\s*};:/i,
91
+ /^kill\s+-9\s+-1/i, /^killall\s+/i, /^pkill\s+-9/i,
92
+ /^systemctl\s+isolate/i, /^systemctl\s+poweroff/i,
93
+ /^systemctl\s+reboot/i, /^systemctl\s+halt/i,
94
+ /^docker\s+rm\s+-f/i, /^docker\s+rmi\s+-f/i, /^docker\s+system\s+prune/i,
95
+ /^kubectl\s+delete\s+all/i, /^kubectl\s+delete\s+namespace/i,
96
+ /^kubectl\s+drain/i, /^kubectl\s+cordon/i,
97
+ /\bnc\s+-[elp]/i, /\/dev\/tcp\//i,
98
+ /python\s+-c\s+.*import\s+socket/i, /perl\s+-e\s+.*socket/i,
99
+ /ruby\s+-e\s+.*socket/i,
100
+ /powershell.*-enc/i, /powershell.*-w\s+hidden/i,
101
+ /iex\s*\(\s*new-object/i, /invoke-expression/i,
102
+ /curl\s+.*\|\s*(ba)?sh/i, /wget\s+.*\|\s*(ba)?sh/i,
103
+ /certutil.*-urlcache.*-f/i, /bitsadmin.*\/transfer/i, /certutil.*-decode/i,
104
+ ];
105
+
106
+ const ASK_PATTERNS = [
107
+ /\?$/, /^help\s/i, /^man\s/i, /^--help/i, /^-h$/, /^--usage/i,
108
+ /^what\s+is\s/i, /^how\s+to\s/i, /^explain\s/i, /^describe\s/i,
109
+ /^why\s/i, /^when\s/i, /^which\s/i, /^where\s/i,
110
+ /^list\s+all\s/i, /^show\s+all\s/i, /^get\s+all\s/i,
111
+ ];
112
+
113
+ /** Tools that are always READ-safe (no confirmation needed) */
114
+ const FILE_READ_TOOLS = ['READ_FILE', 'LIST_DIR', 'SEARCH_FILE', 'TREE_VIEW', 'FILE_INFO', 'FETCH_URL', 'WEB_SEARCH', 'TODO_LIST', 'BROWSE', 'BROWSE_SEARCH', 'BROWSE_EXTRACT', 'SKILL_LIST', 'LOAD_SKILL'];
115
+ const SKILL_WRITE_TOOLS = ['CREATE_SKILL'];
116
+
117
+ /** Tools that are safe WRITE operations in coding mode */
118
+ const CODING_SAFE_TOOLS = ['WRITE_FILE', 'EDIT_FILE', 'APPEND_FILE', 'CREATE_DIR', 'MOVE_FILE', 'COPY_FILE', 'RUN_SCRIPT', 'TODO_ADD', 'TODO_COMPLETE', 'TODO_UPDATE', 'TODO_CLEAR', 'GLOB', 'GREP', 'DIAG_POST_EDIT', 'ASK_USER', 'PLAN_MODE', 'CREATE_SKILL', 'TASK'];
119
+
120
+ export const checkSafety = (command, mode = 'GENERAL') => {
121
+ const toolName = command.tool || '';
122
+ const cmdStr = typeof command === 'string' ? command : (typeof command?.cmd === 'string' ? command.cmd : '');
123
+ const type = command.type;
124
+
125
+ // File read/info tools are always safe
126
+ if (FILE_READ_TOOLS.includes(toolName)) {
127
+ return { tier: TIER.READ, requiresConfirmation: false, isDangerous: false, message: 'Safe read operation' };
128
+ }
129
+
130
+ if (toolName === 'SKILL_LIST' || toolName === 'LOAD_SKILL') {
131
+ return { tier: TIER.READ, requiresConfirmation: false, isDangerous: false, message: 'Safe skill operation' };
132
+ }
133
+
134
+ if (SKILL_WRITE_TOOLS.includes(toolName)) {
135
+ return { tier: TIER.WRITE, requiresConfirmation: mode !== 'CODING', isDangerous: false, message: 'Skill creation' };
136
+ }
137
+
138
+ if (toolName === 'TASK') {
139
+ return { tier: TIER.READ, requiresConfirmation: false, isDangerous: false, message: 'Subagent exploration (read-only, max 1 concurrent)' };
140
+ }
141
+
142
+ // MCP tools default to ASK (user decides per-call)
143
+ if (toolName === 'MCP_TOOL') {
144
+ return { tier: TIER.ASK, requiresConfirmation: false, isDangerous: false, message: 'MCP external tool — user awareness recommended' };
145
+ }
146
+
147
+ // Todo management tools are always safe
148
+ if (toolName.startsWith('TODO_')) {
149
+ return { tier: TIER.READ, requiresConfirmation: false, isDangerous: false, message: 'Todo management operation' };
150
+ }
151
+
152
+ // DELETE_FILE is always WRITE
153
+ if (toolName === 'DELETE_FILE') {
154
+ return { tier: TIER.WRITE, requiresConfirmation: mode !== 'CODING', isDangerous: false, message: 'File deletion operation' };
155
+ }
156
+
157
+ // GIT tool: classify by operation type
158
+ if (toolName === 'GIT') {
159
+ const op = String(command.op || '').toLowerCase().trim();
160
+ const READ_GIT_OPS = ['status', 'log', 'diff', 'branch', 'remote', 'show', 'ls-files', 'ls-tree', 'describe'];
161
+ const DANGEROUS_GIT_OPS = ['push --force', 'reset --hard', 'clean -fd', 'push -f', 'checkout --force'];
162
+ if (READ_GIT_OPS.some(r => op === r || op.startsWith(r + ' '))) {
163
+ return { tier: TIER.READ, requiresConfirmation: false, isDangerous: false, message: 'Git read operation' };
164
+ }
165
+ if (DANGEROUS_GIT_OPS.some(d => op.startsWith(d))) {
166
+ return { tier: TIER.DANGEROUS, requiresConfirmation: true, isDangerous: true, message: 'DANGEROUS git operation — user confirmation required' };
167
+ }
168
+ return { tier: TIER.WRITE, requiresConfirmation: true, isDangerous: false, message: 'Git write operation — confirmation required' };
169
+ }
170
+
171
+ // Coding mode tools auto-approved in CODING mode
172
+ if (mode === 'CODING' && CODING_SAFE_TOOLS.includes(toolName)) {
173
+ return { tier: TIER.WRITE, requiresConfirmation: false, isDangerous: false, message: 'Coding mode write operation (auto-approved)' };
174
+ }
175
+
176
+ if (type === TIER.READ) return { tier: TIER.READ, requiresConfirmation: false, isDangerous: false, message: 'Safe read operation' };
177
+ if (type === TIER.WRITE) return { tier: TIER.WRITE, requiresConfirmation: true, isDangerous: false, message: 'Write operation requires confirmation' };
178
+ if (type === TIER.DANGEROUS) return { tier: TIER.DANGEROUS, requiresConfirmation: true, isDangerous: true, message: 'DANGEROUS operation' };
179
+ if (type === TIER.ASK) return { tier: TIER.ASK, requiresConfirmation: false, isDangerous: false, message: 'Information request' };
180
+
181
+ const lowerCmd = cmdStr.toLowerCase();
182
+ for (const pattern of DANGEROUS_PATTERNS) {
183
+ if (pattern.test(lowerCmd)) return { tier: TIER.DANGEROUS, requiresConfirmation: true, isDangerous: true, message: 'DANGEROUS operation detected' };
184
+ }
185
+ for (const pattern of WRITE_PATTERNS) {
186
+ if (pattern.test(lowerCmd)) {
187
+ if (mode === 'CODING') return { tier: TIER.WRITE, requiresConfirmation: false, isDangerous: false, message: 'Write operation (coding mode auto-approved)' };
188
+ return { tier: TIER.WRITE, requiresConfirmation: true, isDangerous: false, message: 'Write operation requires confirmation' };
189
+ }
190
+ }
191
+ for (const pattern of ASK_PATTERNS) {
192
+ if (pattern.test(lowerCmd)) return { tier: TIER.ASK, requiresConfirmation: false, isDangerous: false, message: 'Information request' };
193
+ }
194
+ for (const pattern of READ_PATTERNS) {
195
+ if (pattern.test(lowerCmd)) return { tier: TIER.READ, requiresConfirmation: false, isDangerous: false, message: 'Safe read operation' };
196
+ }
197
+ return { tier: TIER.WRITE, requiresConfirmation: mode !== 'CODING', isDangerous: false, message: 'Unknown operation — requires confirmation' };
198
+ };
199
+
200
+ export const TIER_LEVELS = TIER;
201
+
202
+ export const getTierColor = (tier) => {
203
+ switch (tier) {
204
+ case TIER.READ: return 'blue';
205
+ case TIER.WRITE: return 'yellow';
206
+ case TIER.DANGEROUS: return 'red';
207
+ case TIER.ASK: return 'green';
208
+ default: return 'gray';
209
+ }
210
+ };
@@ -0,0 +1,78 @@
1
+ import crypto from 'crypto';
2
+ import { logger } from '../utils/logger.js';
3
+
4
+ export const deriveKey = (machineId) => {
5
+ const secret = process.env.AES_SECRET;
6
+ if (secret && typeof secret === 'string' && secret.length > 0) {
7
+ return crypto.createHash('sha256').update(secret).digest();
8
+ }
9
+ if (!machineId || typeof machineId !== 'string' || machineId.length === 0) {
10
+ throw new Error('Cannot derive encryption key: no valid machine ID or AES_SECRET provided');
11
+ }
12
+ return crypto.createHash('sha256').update(machineId).digest();
13
+ };
14
+
15
+ export const encrypt = (text, key) => {
16
+ if (!text || typeof text !== 'string') {
17
+ throw new Error('Cannot encrypt: invalid text (expected non-empty string)');
18
+ }
19
+ if (!key || !Buffer.isBuffer(key) || key.length !== 32) {
20
+ throw new Error('Invalid encryption key: must be 32-byte Buffer');
21
+ }
22
+
23
+ try {
24
+ const iv = crypto.randomBytes(12);
25
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
26
+ let encrypted = cipher.update(text, 'utf8', 'hex');
27
+ encrypted += cipher.final('hex');
28
+ const authTag = cipher.getAuthTag();
29
+ return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
30
+ } catch (err) {
31
+ logger.error('Encryption failed', { error: err.message });
32
+ throw new Error(`Encryption failed: ${err.message}`);
33
+ }
34
+ };
35
+
36
+ export const decrypt = (str, key) => {
37
+ if (!str || typeof str !== 'string') {
38
+ throw new Error('Cannot decrypt: invalid input (expected non-empty string)');
39
+ }
40
+ if (!key || !Buffer.isBuffer(key) || key.length !== 32) {
41
+ throw new Error('Invalid decryption key: must be 32-byte Buffer');
42
+ }
43
+
44
+ const parts = str.split(':');
45
+ if (parts.length !== 3) {
46
+ throw new Error('Cannot decrypt: invalid format (expected iv:authTag:encrypted)');
47
+ }
48
+
49
+ const [ivHex, authTagHex, encrypted] = parts;
50
+
51
+ if (!ivHex || !authTagHex || !encrypted) {
52
+ throw new Error('Cannot decrypt: missing required components (iv, authTag, or encrypted data)');
53
+ }
54
+
55
+ try {
56
+ const iv = Buffer.from(ivHex, 'hex');
57
+ const authTag = Buffer.from(authTagHex, 'hex');
58
+
59
+ if (iv.length !== 12) {
60
+ throw new Error('Invalid IV length (expected 12 bytes)');
61
+ }
62
+ if (authTag.length !== 16) {
63
+ throw new Error('Invalid auth tag length (expected 16 bytes)');
64
+ }
65
+
66
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
67
+ decipher.setAuthTag(authTag);
68
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
69
+ decrypted += decipher.final('utf8');
70
+ return decrypted;
71
+ } catch (err) {
72
+ logger.error('Decryption failed', { error: err.message });
73
+ if (err.message.includes('Unsupported state')) {
74
+ throw new Error('Decryption failed: authentication tag mismatch (possible data corruption or key mismatch)');
75
+ }
76
+ throw new Error(`Decryption failed: ${err.message}`);
77
+ }
78
+ };
@@ -0,0 +1,68 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Command Executor
3
+ // =============================================================================
4
+ // Unified command executor that dispatches to local or SSH based on device.
5
+ // Acts as a bridge between the agent loop and the tool system.
6
+ // =============================================================================
7
+ import { exec } from 'child_process';
8
+ import { connectSSH, execSSH, closeSSH } from './ssh.js';
9
+ import { DEFAULTS } from '../utils/constants.js';
10
+ import { logger } from '../utils/logger.js';
11
+
12
+ const COMMAND_TIMEOUT = DEFAULTS.COMMAND_TIMEOUT;
13
+
14
+ const withTimeout = (promise, ms) => {
15
+ let timer;
16
+ return Promise.race([
17
+ promise.then(result => { clearTimeout(timer); return result; }),
18
+ new Promise((_, reject) => {
19
+ timer = setTimeout(() => reject(new Error('Timeout')), ms);
20
+ }),
21
+ ]);
22
+ };
23
+
24
+ /**
25
+ * Execute a command either locally or on a remote device via SSH.
26
+ * Automatically detects whether the target is local or remote.
27
+ */
28
+ export const executeCommand = async ({ cmd, cmdType, device, authDecrypted }) => {
29
+ const isLocal = !device?.ip || device.ip === 'localhost' || device.ip === '127.0.0.1' || device?.type === 'Local';
30
+
31
+ if (isLocal) {
32
+ logger.debug('Executing local command', { cmd: cmd.slice(0, 100) });
33
+ return withTimeout(
34
+ new Promise((resolve) => {
35
+ exec(cmd, (error, stdout, stderr) => {
36
+ if (error) {
37
+ resolve({ output: stderr || error.message, status: 'error' });
38
+ } else {
39
+ resolve({ output: stdout + stderr, status: 'success' });
40
+ }
41
+ });
42
+ }),
43
+ COMMAND_TIMEOUT
44
+ );
45
+ }
46
+
47
+ try {
48
+ logger.debug('Executing SSH command', { host: device.ip, cmd: cmd.slice(0, 100) });
49
+ let conn = await connectSSH(
50
+ {
51
+ host: device.ip,
52
+ port: device.port || 22,
53
+ username: authDecrypted.username,
54
+ password: authDecrypted.password,
55
+ privateKey: authDecrypted.privateKey || null,
56
+ },
57
+ device.type
58
+ );
59
+ return await withTimeout(execSSH(conn, cmd), COMMAND_TIMEOUT);
60
+ } catch (err) {
61
+ logger.error('SSH command execution failed', { host: device.ip, error: err.message });
62
+ return { output: err.message, status: 'error' };
63
+ }
64
+ };
65
+
66
+ export const closeConnection = (device) => {
67
+ if (device?.ip) closeSSH(device.ip);
68
+ };
@@ -0,0 +1,58 @@
1
+ import { DEFAULTS } from '../utils/constants.js';
2
+
3
+ export class CommandHistory {
4
+ constructor(maxSize = DEFAULTS.MAX_COMMAND_HISTORY) {
5
+ this.maxSize = maxSize;
6
+ this.commands = [];
7
+ this.currentIndex = -1;
8
+ this.partialInput = '';
9
+ }
10
+
11
+ add(command) {
12
+ if (!command || !command.trim()) return;
13
+ const trimmed = command.trim();
14
+ // Skip duplicates of the most recent command
15
+ if (this.commands.length > 0 && this.commands[this.commands.length - 1] === trimmed) return;
16
+ this.commands.push(trimmed);
17
+ if (this.commands.length > this.maxSize) this.commands.shift();
18
+ this.currentIndex = -1;
19
+ this.partialInput = '';
20
+ }
21
+
22
+ getPrevious(partialInput) {
23
+ if (this.currentIndex === -1) {
24
+ this.partialInput = partialInput || '';
25
+ this.currentIndex = this.commands.length;
26
+ }
27
+ if (this.currentIndex > 0) {
28
+ this.currentIndex--;
29
+ return this.commands[this.currentIndex];
30
+ }
31
+ return this.commands[0] || null;
32
+ }
33
+
34
+ getNext() {
35
+ if (this.currentIndex === -1) return this.partialInput;
36
+ if (this.currentIndex < this.commands.length - 1) {
37
+ this.currentIndex++;
38
+ return this.commands[this.currentIndex];
39
+ }
40
+ this.currentIndex = -1;
41
+ return this.partialInput;
42
+ }
43
+
44
+ search(prefix) {
45
+ if (!prefix) return [];
46
+ const lower = prefix.toLowerCase();
47
+ return this.commands.filter(cmd => cmd.toLowerCase().includes(lower));
48
+ }
49
+
50
+ clear() {
51
+ this.commands = [];
52
+ this.currentIndex = -1;
53
+ this.partialInput = '';
54
+ }
55
+
56
+ getAll() { return [...this.commands]; }
57
+ getRecent(count = 10) { return this.commands.slice(-count); }
58
+ }
@@ -0,0 +1,11 @@
1
+ export const DEFAULT_SERVER_URL = 'ws://localhost:3001';
2
+
3
+ export const toHttpUrl = (serverUrl) => {
4
+ if (!serverUrl) return null;
5
+ return serverUrl.replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://');
6
+ };
7
+
8
+ export const toWsUrl = (serverUrl) => {
9
+ if (!serverUrl) return null;
10
+ return serverUrl.replace(/^https:\/\//, 'wss://').replace(/^http:\/\//, 'ws://');
11
+ };
@@ -0,0 +1,194 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Session Manager
3
+ // Supports both LOCAL (JSON file) and CLOUD (MongoDB via server API) storage.
4
+ // =============================================================================
5
+ import fs from 'fs/promises';
6
+ import { mkdirSync, writeFileSync } from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import { logger } from '../utils/logger.js';
10
+
11
+ const SESSIONS_DIR = path.join(os.homedir(), '.osai-agent', 'sessions');
12
+
13
+ const ensureDir = async () => { await fs.mkdir(SESSIONS_DIR, { recursive: true }); };
14
+
15
+ const makeId = () => {
16
+ const now = new Date();
17
+ const d = `${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`;
18
+ const t = `${String(now.getHours()).padStart(2,'0')}${String(now.getMinutes()).padStart(2,'0')}`;
19
+ return `${d}-${t}-${Math.random().toString(36).slice(2,6)}`;
20
+ };
21
+
22
+ const apiFetch = async (server, token, endpoint, opts = {}) => {
23
+ const res = await fetch(`${server}${endpoint}`, {
24
+ ...opts,
25
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...(opts.headers||{}) },
26
+ signal: AbortSignal.timeout(8000),
27
+ });
28
+ if (!res.ok) throw new Error(`Cloud ${res.status}: ${await res.text()}`);
29
+ return res.json();
30
+ };
31
+
32
+ export class SessionManager {
33
+ constructor({ server = null, token = null } = {}) {
34
+ this.sessionsDir = SESSIONS_DIR;
35
+ this.server = server;
36
+ this.token = token;
37
+ }
38
+
39
+ generateSessionId() { return makeId(); }
40
+
41
+ // ── LOCAL ────────────────────────────────────────────────────────────────
42
+
43
+ async saveLocal(sessionId, data) {
44
+ await ensureDir();
45
+ const fp = path.join(this.sessionsDir, `${sessionId}.json`);
46
+ await fs.writeFile(fp, JSON.stringify({ id: sessionId, savedAt: new Date().toISOString(), storage: 'local', ...data }, null, 2), 'utf-8');
47
+ logger.debug('Session saved locally', { id: sessionId });
48
+ return fp;
49
+ }
50
+
51
+ saveLocalSync(sessionId, data) {
52
+ mkdirSync(this.sessionsDir, { recursive: true });
53
+ const fp = path.join(this.sessionsDir, `${sessionId}.json`);
54
+ writeFileSync(fp, JSON.stringify({ id: sessionId, savedAt: new Date().toISOString(), storage: 'local', ...data }, null, 2), 'utf-8');
55
+ logger.debug('Session saved locally (sync)', { id: sessionId });
56
+ return fp;
57
+ }
58
+
59
+ async loadLocal(sessionId) {
60
+ try { return JSON.parse(await fs.readFile(path.join(this.sessionsDir, `${sessionId}.json`), 'utf-8')); }
61
+ catch { return null; }
62
+ }
63
+
64
+ async listLocal() {
65
+ try {
66
+ await ensureDir();
67
+ const files = await fs.readdir(this.sessionsDir);
68
+ const sessions = [];
69
+ for (const file of files) {
70
+ if (!file.endsWith('.json')) continue;
71
+ try {
72
+ const data = JSON.parse(await fs.readFile(path.join(this.sessionsDir, file), 'utf-8'));
73
+ sessions.push({
74
+ id: data.id,
75
+ title: data.title || data.conversationHistory?.find(m => m.role === 'user')?.content?.slice(0, 60) || 'Untitled',
76
+ savedAt: data.savedAt,
77
+ storage: 'local',
78
+ messageCount: data.conversationHistory?.length || 0,
79
+ mode: data.mode || 'LOCAL',
80
+ os: data.os || 'unknown',
81
+ });
82
+ } catch { /* skip corrupt */ }
83
+ }
84
+ return sessions.sort((a, b) => new Date(b.savedAt) - new Date(a.savedAt));
85
+ } catch { return []; }
86
+ }
87
+
88
+ async deleteLocal(sessionId) {
89
+ try { await fs.unlink(path.join(this.sessionsDir, `${sessionId}.json`)); return true; }
90
+ catch { return false; }
91
+ }
92
+
93
+ // ── CLOUD ────────────────────────────────────────────────────────────────
94
+
95
+ async saveCloud(sessionId, data) {
96
+ if (!this.server || !this.token) throw new Error('Not connected to server');
97
+ const result = await apiFetch(this.server, this.token, '/conversations', {
98
+ method: 'POST',
99
+ body: JSON.stringify({
100
+ session_id: sessionId,
101
+ title: data.title || null,
102
+ os: data.os || 'unknown',
103
+ mode: data.mode || 'LOCAL',
104
+ messages: data.conversationHistory || [],
105
+ stats: data.stats || {},
106
+ }),
107
+ });
108
+ logger.debug('Session saved to cloud', { id: sessionId });
109
+ return result;
110
+ }
111
+
112
+ async loadCloud(sessionId) {
113
+ if (!this.server || !this.token) throw new Error('Not connected to server');
114
+ const conv = await apiFetch(this.server, this.token, `/conversations/${sessionId}`);
115
+ return {
116
+ id: conv.session_id,
117
+ savedAt: conv.updated_at,
118
+ storage: 'cloud',
119
+ os: conv.os,
120
+ mode: conv.mode,
121
+ conversationHistory: (conv.messages || []).map(m => ({ role: m.role, content: m.content })),
122
+ stats: conv.stats || {},
123
+ };
124
+ }
125
+
126
+ async listCloud(page = 1) {
127
+ if (!this.server || !this.token) throw new Error('Not connected to server');
128
+ const data = await apiFetch(this.server, this.token, `/conversations?page=${page}&limit=20`);
129
+ return (data.conversations || []).map(c => ({
130
+ id: c.session_id,
131
+ title: c.title || 'Untitled',
132
+ savedAt: c.updated_at,
133
+ storage: 'cloud',
134
+ messageCount: (c.messages || []).length,
135
+ mode: c.mode || 'LOCAL',
136
+ os: c.os || 'unknown',
137
+ }));
138
+ }
139
+
140
+ async deleteCloud(sessionId) {
141
+ if (!this.server || !this.token) throw new Error('Not connected to server');
142
+ return apiFetch(this.server, this.token, `/conversations/${sessionId}`, { method: 'DELETE' });
143
+ }
144
+
145
+ // ── UNIFIED ──────────────────────────────────────────────────────────────
146
+
147
+ async listAll() {
148
+ const [local, cloud] = await Promise.allSettled([
149
+ this.listLocal(),
150
+ (this.server && this.token) ? this.listCloud() : Promise.resolve([]),
151
+ ]);
152
+ const localList = local.status === 'fulfilled' ? local.value : [];
153
+ const cloudList = cloud.status === 'fulfilled' ? cloud.value : [];
154
+ const seen = new Set();
155
+ const merged = [];
156
+ for (const s of [...cloudList, ...localList]) {
157
+ if (!seen.has(s.id)) { seen.add(s.id); merged.push(s); }
158
+ }
159
+ return merged.sort((a, b) => new Date(b.savedAt) - new Date(a.savedAt));
160
+ }
161
+
162
+ async load(sessionId) {
163
+ if (this.server && this.token) {
164
+ try { return await this.loadCloud(sessionId); } catch {}
165
+ }
166
+ return this.loadLocal(sessionId);
167
+ }
168
+
169
+ // Legacy alias
170
+ async saveSession(sessionId, data, storage = 'local') {
171
+ if (storage === 'cloud') return this.saveCloud(sessionId, data);
172
+ return this.saveLocal(sessionId, data);
173
+ }
174
+
175
+ async exportSession(sessionId, format = 'markdown') {
176
+ const session = await this.load(sessionId);
177
+ if (!session) return null;
178
+ if (format === 'json') return JSON.stringify(session, null, 2);
179
+ const lines = [`# Session ${session.id}`, `Mode: ${session.mode} | OS: ${session.os} | Saved: ${new Date(session.savedAt).toLocaleString()}`, ''];
180
+ for (const msg of (session.conversationHistory || [])) {
181
+ lines.push(`**${msg.role}:** ${msg.content}`);
182
+ }
183
+ return lines.join('\n');
184
+ }
185
+
186
+ async deleteSession(sessionId) {
187
+ let deleted = false;
188
+ if (this.server && this.token) {
189
+ try { await this.deleteCloud(sessionId); deleted = true; } catch {}
190
+ }
191
+ if (!deleted) deleted = await this.deleteLocal(sessionId);
192
+ return deleted;
193
+ }
194
+ }