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,317 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
3
|
+
import Conf from 'conf';
|
|
4
|
+
import { decrypt, deriveKey } from '../services/crypto.js';
|
|
5
|
+
import pkg from 'node-machine-id';
|
|
6
|
+
const { machineIdSync } = pkg;
|
|
7
|
+
|
|
8
|
+
const SDK_DEFAULT_MODELS = {
|
|
9
|
+
anthropic: 'claude-sonnet-4-6',
|
|
10
|
+
gemini: 'gemini-2.5-flash',
|
|
11
|
+
openai: 'gpt-5.4-mini',
|
|
12
|
+
groq: 'llama-3.3-70b-versatile',
|
|
13
|
+
mistral: 'mistral-large-2506',
|
|
14
|
+
deepseek: 'deepseek-chat',
|
|
15
|
+
xai: 'grok-3',
|
|
16
|
+
cohere: 'command-r-plus',
|
|
17
|
+
perplexity: 'sonar-pro',
|
|
18
|
+
together: 'mistralai/Mixtral-8x22B-Instruct-v0.1',
|
|
19
|
+
fireworks: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
20
|
+
cerebras: 'llama3.1-8b',
|
|
21
|
+
openrouter: 'auto',
|
|
22
|
+
ollama: 'llama3.2',
|
|
23
|
+
'ollama-cloud': 'llama3.2',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_BASE_URLS = {
|
|
27
|
+
openai: 'https://api.openai.com/v1',
|
|
28
|
+
anthropic: 'https://api.anthropic.com',
|
|
29
|
+
gemini: 'https://generativelanguage.googleapis.com',
|
|
30
|
+
groq: 'https://api.groq.com/openai/v1',
|
|
31
|
+
mistral: 'https://api.mistral.ai/v1',
|
|
32
|
+
deepseek: 'https://api.deepseek.com/v1',
|
|
33
|
+
xai: 'https://api.x.ai/v1',
|
|
34
|
+
cohere: 'https://api.cohere.ai/compatibility/v1',
|
|
35
|
+
perplexity: 'https://api.perplexity.ai',
|
|
36
|
+
together: 'https://api.together.ai/v1',
|
|
37
|
+
fireworks: 'https://api.fireworks.ai/inference/v1',
|
|
38
|
+
cerebras: 'https://api.cerebras.ai/v1',
|
|
39
|
+
openrouter: 'https://openrouter.ai/api/v1',
|
|
40
|
+
huggingface: 'https://router.huggingface.co/v1',
|
|
41
|
+
github: 'https://models.github.ai/inference',
|
|
42
|
+
siliconflow: 'https://api.siliconflow.cn/v1',
|
|
43
|
+
hyperbolic: 'https://api.hyperbolic.xyz/v1',
|
|
44
|
+
novita: 'https://api.novita.ai/v3/openai',
|
|
45
|
+
deepinfra: 'https://api.deepinfra.com/v1/openai',
|
|
46
|
+
qwen: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
47
|
+
moonshot: 'https://api.moonshot.ai/v1',
|
|
48
|
+
zhipu: 'https://open.bigmodel.cn/api/paas/v4',
|
|
49
|
+
yi: 'https://api.lingyiwanwu.com/v1',
|
|
50
|
+
nvidia: 'https://integrate.api.nvidia.com/v1',
|
|
51
|
+
baidu: 'https://qianfan.baidubce.com/v2',
|
|
52
|
+
codestral: 'https://codestral.mistral.ai/v1',
|
|
53
|
+
nebius: 'https://api.studio.nebius.ai/v1',
|
|
54
|
+
ollama: 'http://localhost:11434/v1',
|
|
55
|
+
'ollama-cloud': 'https://ollama.com/v1',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function getLocalKey() {
|
|
59
|
+
return deriveKey(machineIdSync());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getLocalProviderConfig() {
|
|
63
|
+
const config = new Conf({ projectName: 'osai-agent' });
|
|
64
|
+
// migrate from old single-provider format
|
|
65
|
+
const old = config.get('localProvider');
|
|
66
|
+
if (old && !config.get('localProviders')) {
|
|
67
|
+
const parsed = typeof old === 'string' ? JSON.parse(old) : old;
|
|
68
|
+
const { type, apiKeyEnc, model, baseUrl } = parsed;
|
|
69
|
+
if (type) {
|
|
70
|
+
config.set('localProviders', { [type]: { apiKeyEnc: apiKeyEnc || null, model: model || null, baseUrl: baseUrl || null } });
|
|
71
|
+
config.set('currentLocalProvider', type);
|
|
72
|
+
}
|
|
73
|
+
config.delete('localProvider');
|
|
74
|
+
}
|
|
75
|
+
const current = config.get('currentLocalProvider');
|
|
76
|
+
if (!current) return null;
|
|
77
|
+
const providers = config.get('localProviders', {});
|
|
78
|
+
const entry = providers[current];
|
|
79
|
+
if (!entry) return null;
|
|
80
|
+
try {
|
|
81
|
+
const key = getLocalKey();
|
|
82
|
+
const apiKey = entry.apiKeyEnc ? decrypt(entry.apiKeyEnc, key) : null;
|
|
83
|
+
return {
|
|
84
|
+
type: current,
|
|
85
|
+
apiKey,
|
|
86
|
+
model: entry.model || SDK_DEFAULT_MODELS[current] || SDK_DEFAULT_MODELS.openai,
|
|
87
|
+
baseUrl: entry.baseUrl || DEFAULT_BASE_URLS[current] || null,
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function fetchLocalModels({ type, apiKey, baseUrl }) {
|
|
95
|
+
if (type === 'anthropic') {
|
|
96
|
+
const res = await fetch('https://api.anthropic.com/v1/models', {
|
|
97
|
+
headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) throw new Error(`Anthropic API ${res.status}`);
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
return (data.data || []).map(m => m.id || m.name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (type === 'gemini') {
|
|
105
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
|
|
106
|
+
if (!res.ok) throw new Error(`Gemini API ${res.status}`);
|
|
107
|
+
const data = await res.json();
|
|
108
|
+
return (data.models || []).map(m => m.name.replace('models/', '')).filter(n => !n.includes('/'));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const url = (baseUrl || DEFAULT_BASE_URLS[type] || '').replace(/\/+$/, '');
|
|
112
|
+
if (!url) throw new Error(`No base URL for ${type}`);
|
|
113
|
+
const hasApiVersion = /\/v\d+$/.test(url);
|
|
114
|
+
const modelsPath = hasApiVersion ? '/models' : '/v1/models';
|
|
115
|
+
const res = await fetch(`${url}${modelsPath}`, {
|
|
116
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
117
|
+
});
|
|
118
|
+
if (!res.ok) throw new Error(`${type} API ${res.status}`);
|
|
119
|
+
const data = await res.json();
|
|
120
|
+
return (data.data || []).map(m => m.id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createLocalOpenAiClient({ apiKey, baseUrl, defaultHeaders = {} }) {
|
|
124
|
+
return new OpenAI({
|
|
125
|
+
apiKey: apiKey || 'no-key-required',
|
|
126
|
+
baseURL: baseUrl || undefined,
|
|
127
|
+
defaultHeaders: Object.keys(defaultHeaders).length ? defaultHeaders : undefined,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function callLocalCompletion({ provider, message, signal }) {
|
|
132
|
+
if (provider.type === 'anthropic') {
|
|
133
|
+
const resp = await fetch('https://api.anthropic.com/v1/messages', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
'x-api-key': provider.apiKey,
|
|
138
|
+
'anthropic-version': '2023-06-01',
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
model: provider.model,
|
|
142
|
+
max_tokens: 4096,
|
|
143
|
+
messages: [{ role: 'user', content: message }],
|
|
144
|
+
}),
|
|
145
|
+
signal,
|
|
146
|
+
});
|
|
147
|
+
if (!resp.ok) {
|
|
148
|
+
const detail = await resp.text().catch(() => '');
|
|
149
|
+
throw new Error(`Anthropic API Error (${resp.status})${detail ? `: ${detail}` : ''}`);
|
|
150
|
+
}
|
|
151
|
+
const data = await resp.json();
|
|
152
|
+
return data.content?.[0]?.text || '';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (provider.type === 'gemini') {
|
|
156
|
+
const genAI = new GoogleGenerativeAI(provider.apiKey);
|
|
157
|
+
const model = genAI.getGenerativeModel({ model: provider.model });
|
|
158
|
+
const result = await model.generateContent(message);
|
|
159
|
+
return result.response.text() || '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const client = createLocalOpenAiClient({
|
|
163
|
+
apiKey: provider.apiKey,
|
|
164
|
+
baseUrl: provider.baseUrl,
|
|
165
|
+
});
|
|
166
|
+
const completion = await client.chat.completions.create({
|
|
167
|
+
model: provider.model,
|
|
168
|
+
messages: [{ role: 'user', content: message }],
|
|
169
|
+
});
|
|
170
|
+
return completion.choices?.[0]?.message?.content || '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractOpenAiDeltaText(chunk) {
|
|
174
|
+
const delta = chunk?.choices?.[0]?.delta;
|
|
175
|
+
if (!delta) return '';
|
|
176
|
+
return delta.content || delta.reasoning_content || delta.text || '';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function streamLocalCompletion({
|
|
180
|
+
provider, systemPrompt, history, message, signal,
|
|
181
|
+
}) {
|
|
182
|
+
const encoder = new TextEncoder();
|
|
183
|
+
const effectiveModel = provider.model || SDK_DEFAULT_MODELS[provider.type] || SDK_DEFAULT_MODELS.openai;
|
|
184
|
+
|
|
185
|
+
const sendChunk = (controller, content) => {
|
|
186
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
|
187
|
+
type: 'chunk',
|
|
188
|
+
content,
|
|
189
|
+
provider: provider.type,
|
|
190
|
+
model: effectiveModel,
|
|
191
|
+
})}\n\n`));
|
|
192
|
+
};
|
|
193
|
+
const sendDone = (controller) => {
|
|
194
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const messages = [
|
|
198
|
+
{ role: 'system', content: systemPrompt },
|
|
199
|
+
...(history || [])
|
|
200
|
+
.filter(h => h.role === 'user' || h.role === 'assistant')
|
|
201
|
+
.map(h => ({ role: h.role, content: typeof h.content === 'string' ? h.content : JSON.stringify(h.content) })),
|
|
202
|
+
{ role: 'user', content: message },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
return new ReadableStream({
|
|
206
|
+
async start(controller) {
|
|
207
|
+
try {
|
|
208
|
+
if (provider.type === 'anthropic') {
|
|
209
|
+
const resp = await fetch('https://api.anthropic.com/v1/messages', {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: {
|
|
212
|
+
'Content-Type': 'application/json',
|
|
213
|
+
'x-api-key': provider.apiKey,
|
|
214
|
+
'anthropic-version': '2023-06-01',
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
model: effectiveModel,
|
|
218
|
+
max_tokens: 4096,
|
|
219
|
+
stream: true,
|
|
220
|
+
system: systemPrompt,
|
|
221
|
+
messages: (history || [])
|
|
222
|
+
.filter(h => h.role === 'user' || h.role === 'assistant')
|
|
223
|
+
.map(h => ({
|
|
224
|
+
role: h.role === 'assistant' ? 'assistant' : 'user',
|
|
225
|
+
content: typeof h.content === 'string' ? h.content : JSON.stringify(h.content),
|
|
226
|
+
})),
|
|
227
|
+
}),
|
|
228
|
+
signal,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (!resp.ok) {
|
|
232
|
+
const detail = await resp.text().catch(() => '');
|
|
233
|
+
throw new Error(`Anthropic API error (${resp.status})${detail ? `: ${detail}` : ''}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
237
|
+
if (contentType.includes('text/event-stream') && resp.body) {
|
|
238
|
+
const reader = resp.body.getReader();
|
|
239
|
+
const decoder = new TextDecoder();
|
|
240
|
+
let buffer = '';
|
|
241
|
+
while (true) {
|
|
242
|
+
if (signal?.aborted) break;
|
|
243
|
+
const { done, value } = await reader.read();
|
|
244
|
+
if (done) break;
|
|
245
|
+
buffer += decoder.decode(value, { stream: true });
|
|
246
|
+
const lines = buffer.split('\n');
|
|
247
|
+
buffer = lines.pop() || '';
|
|
248
|
+
for (const line of lines) {
|
|
249
|
+
if (!line.startsWith('data: ')) continue;
|
|
250
|
+
const payload = line.slice(6).trim();
|
|
251
|
+
if (!payload || payload === '[DONE]') continue;
|
|
252
|
+
try {
|
|
253
|
+
const event = JSON.parse(payload);
|
|
254
|
+
if (event.type === 'content_block_delta' && event.delta?.text) {
|
|
255
|
+
sendChunk(controller, event.delta.text);
|
|
256
|
+
}
|
|
257
|
+
} catch {}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
const data = await resp.json();
|
|
262
|
+
const text = (data.content || [])
|
|
263
|
+
.filter(part => part?.type === 'text' && part.text)
|
|
264
|
+
.map(part => part.text)
|
|
265
|
+
.join('');
|
|
266
|
+
if (text) sendChunk(controller, text);
|
|
267
|
+
}
|
|
268
|
+
} else if (provider.type === 'gemini') {
|
|
269
|
+
const genAI = new GoogleGenerativeAI(provider.apiKey);
|
|
270
|
+
const geminiModel = genAI.getGenerativeModel({ model: effectiveModel });
|
|
271
|
+
const geminiHistory = messages
|
|
272
|
+
.filter(m => m.role !== 'system')
|
|
273
|
+
.slice(0, -1)
|
|
274
|
+
.map(m => ({
|
|
275
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
276
|
+
parts: [{ text: m.content }],
|
|
277
|
+
}));
|
|
278
|
+
const chat = geminiModel.startChat({
|
|
279
|
+
history: geminiHistory,
|
|
280
|
+
systemInstruction: systemPrompt,
|
|
281
|
+
});
|
|
282
|
+
const result = await chat.sendMessageStream(message);
|
|
283
|
+
for await (const chunk of result.stream) {
|
|
284
|
+
if (signal?.aborted) break;
|
|
285
|
+
try {
|
|
286
|
+
const text = chunk.text();
|
|
287
|
+
if (text) sendChunk(controller, text);
|
|
288
|
+
} catch {}
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
const client = createLocalOpenAiClient({
|
|
292
|
+
apiKey: provider.apiKey,
|
|
293
|
+
baseUrl: provider.baseUrl,
|
|
294
|
+
});
|
|
295
|
+
const completion = await client.chat.completions.create({
|
|
296
|
+
model: effectiveModel,
|
|
297
|
+
messages,
|
|
298
|
+
stream: true,
|
|
299
|
+
}, { signal });
|
|
300
|
+
|
|
301
|
+
for await (const chunk of completion) {
|
|
302
|
+
if (signal?.aborted) break;
|
|
303
|
+
const text = extractOpenAiDeltaText(chunk);
|
|
304
|
+
if (text) sendChunk(controller, text);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
sendDone(controller);
|
|
308
|
+
controller.close();
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (err.name !== 'AbortError') {
|
|
311
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'error', message: err.message })}\n\n`));
|
|
312
|
+
}
|
|
313
|
+
controller.close();
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// OS AI Agent — Memory Store
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Lightweight JSON-based memory system that stores actions, results, and
|
|
5
|
+
// failures across sessions. Enables the agent to learn from past interactions.
|
|
6
|
+
// Implements a 6-layer memory model inspired by Claude Code's architecture:
|
|
7
|
+
// 1. System Prompt (static)
|
|
8
|
+
// 2. Session Messages (current conversation)
|
|
9
|
+
// 3. Compact History (auto-trimmed)
|
|
10
|
+
// 4. Project Knowledge (instructions.md)
|
|
11
|
+
// 5. Persistent Memory (this store - actions/failures/preferences)
|
|
12
|
+
// 6. Semantic Search (future — keyword-based for now)
|
|
13
|
+
// =============================================================================
|
|
14
|
+
import fs from 'fs/promises';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
import { DEFAULTS } from '../utils/constants.js';
|
|
18
|
+
import { logger } from '../utils/logger.js';
|
|
19
|
+
|
|
20
|
+
const MEMORY_DIR = path.join(os.homedir(), '.osai-agent', 'memory');
|
|
21
|
+
const MEMORY_FILE = path.join(MEMORY_DIR, 'memory.json');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* MemoryStore — Persistent JSON-based memory for the agent.
|
|
25
|
+
*/
|
|
26
|
+
export class MemoryStore {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.memory = null;
|
|
29
|
+
this.loaded = false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async _ensureLoaded() {
|
|
33
|
+
if (this.loaded) return;
|
|
34
|
+
try {
|
|
35
|
+
await fs.mkdir(MEMORY_DIR, { recursive: true });
|
|
36
|
+
try {
|
|
37
|
+
const data = await fs.readFile(MEMORY_FILE, 'utf-8');
|
|
38
|
+
this.memory = JSON.parse(data);
|
|
39
|
+
} catch {
|
|
40
|
+
this.memory = this._emptyMemory();
|
|
41
|
+
}
|
|
42
|
+
this.loaded = true;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
logger.error('Failed to initialize memory store', { error: err.message });
|
|
45
|
+
this.memory = this._emptyMemory();
|
|
46
|
+
this.loaded = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_emptyMemory() {
|
|
51
|
+
return {
|
|
52
|
+
version: 2,
|
|
53
|
+
created: new Date().toISOString(),
|
|
54
|
+
updated: new Date().toISOString(),
|
|
55
|
+
actions: [],
|
|
56
|
+
results: [],
|
|
57
|
+
failures: [],
|
|
58
|
+
preferences: {},
|
|
59
|
+
patterns: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async _save() {
|
|
64
|
+
try {
|
|
65
|
+
this.memory.updated = new Date().toISOString();
|
|
66
|
+
this._trimEntries();
|
|
67
|
+
await fs.writeFile(MEMORY_FILE, JSON.stringify(this.memory, null, 2), 'utf-8');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
logger.error('Failed to save memory', { error: err.message });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_trimEntries() {
|
|
74
|
+
const max = DEFAULTS.MAX_MEMORY_ENTRIES;
|
|
75
|
+
for (const key of ['actions', 'results', 'failures', 'patterns']) {
|
|
76
|
+
if (this.memory[key].length > max) {
|
|
77
|
+
this.memory[key] = this.memory[key].slice(-max);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async recordAction(entry) {
|
|
83
|
+
await this._ensureLoaded();
|
|
84
|
+
this.memory.actions.push({ ...entry, timestamp: new Date().toISOString() });
|
|
85
|
+
await this._save();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async recordResult(entry) {
|
|
89
|
+
await this._ensureLoaded();
|
|
90
|
+
this.memory.results.push({ ...entry, timestamp: new Date().toISOString() });
|
|
91
|
+
await this._save();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async recordFailure(entry) {
|
|
95
|
+
await this._ensureLoaded();
|
|
96
|
+
this.memory.failures.push({ ...entry, timestamp: new Date().toISOString() });
|
|
97
|
+
await this._save();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async setPreference(key, value) {
|
|
101
|
+
await this._ensureLoaded();
|
|
102
|
+
this.memory.preferences[key] = { value, updatedAt: new Date().toISOString() };
|
|
103
|
+
await this._save();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getPreference(key, defaultValue = null) {
|
|
107
|
+
await this._ensureLoaded();
|
|
108
|
+
const pref = this.memory.preferences[key];
|
|
109
|
+
return pref ? pref.value : defaultValue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getRecentActions(count = 10) {
|
|
113
|
+
await this._ensureLoaded();
|
|
114
|
+
return this.memory.actions.slice(-count);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async getRecentFailures(count = 10) {
|
|
118
|
+
await this._ensureLoaded();
|
|
119
|
+
return this.memory.failures.slice(-count);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async search(query, count = 20) {
|
|
123
|
+
await this._ensureLoaded();
|
|
124
|
+
const lower = query.toLowerCase();
|
|
125
|
+
const results = [];
|
|
126
|
+
for (const action of this.memory.actions) {
|
|
127
|
+
if (JSON.stringify(action).toLowerCase().includes(lower)) {
|
|
128
|
+
results.push({ type: 'action', ...action });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const failure of this.memory.failures) {
|
|
132
|
+
if (JSON.stringify(failure).toLowerCase().includes(lower)) {
|
|
133
|
+
results.push({ type: 'failure', ...failure });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return results.slice(-count);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async getStats() {
|
|
140
|
+
await this._ensureLoaded();
|
|
141
|
+
return {
|
|
142
|
+
totalActions: this.memory.actions.length,
|
|
143
|
+
totalResults: this.memory.results.length,
|
|
144
|
+
totalFailures: this.memory.failures.length,
|
|
145
|
+
totalPreferences: Object.keys(this.memory.preferences).length,
|
|
146
|
+
lastUpdated: this.memory.updated,
|
|
147
|
+
created: this.memory.created,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build a context summary for the LLM prompt.
|
|
153
|
+
* Includes recent failures to avoid and recent successful patterns.
|
|
154
|
+
*/
|
|
155
|
+
async buildContextSummary(maxEntries = 5) {
|
|
156
|
+
await this._ensureLoaded();
|
|
157
|
+
const lines = [];
|
|
158
|
+
|
|
159
|
+
const recentFailures = this.memory.failures.slice(-maxEntries);
|
|
160
|
+
if (recentFailures.length > 0) {
|
|
161
|
+
lines.push('RECENT FAILURES TO AVOID:');
|
|
162
|
+
for (const f of recentFailures) {
|
|
163
|
+
lines.push(` - ${f.command || f.tool}: ${f.error || f.reason}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const recentActions = this.memory.actions.slice(-maxEntries);
|
|
168
|
+
if (recentActions.length > 0) {
|
|
169
|
+
lines.push('RECENT SUCCESSFUL ACTIONS:');
|
|
170
|
+
for (const a of recentActions) {
|
|
171
|
+
lines.push(` - ${a.command || a.tool}: ${a.status || 'completed'}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const recentVerification = this.memory.results
|
|
176
|
+
.slice()
|
|
177
|
+
.reverse()
|
|
178
|
+
.find((r) => r && r.verification);
|
|
179
|
+
if (recentVerification?.verification) {
|
|
180
|
+
const v = recentVerification.verification;
|
|
181
|
+
lines.push('VERIFICATION METRICS:');
|
|
182
|
+
lines.push(` - Files read (ops): ${v.filesReadOps ?? 0}`);
|
|
183
|
+
lines.push(` - Files read (unique): ${v.filesReadUnique ?? 0}`);
|
|
184
|
+
lines.push(` - Blocked writes without read: ${v.blockedWritesWithoutRead ?? 0}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const verificationState = this.memory.preferences?.verification_state?.value;
|
|
188
|
+
if (verificationState) {
|
|
189
|
+
lines.push('PERSISTENT VERIFICATION STATE:');
|
|
190
|
+
lines.push(` - Freshness window: ${verificationState.freshnessWindow ?? 20} interactions`);
|
|
191
|
+
lines.push(` - Current interaction index: ${verificationState.interactionIndex ?? 0}`);
|
|
192
|
+
lines.push(` - Tracked files: ${Object.keys(verificationState.trackedFiles || {}).length}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Include preferences if any
|
|
196
|
+
const prefKeys = Object.keys(this.memory.preferences);
|
|
197
|
+
if (prefKeys.length > 0) {
|
|
198
|
+
lines.push('USER PREFERENCES:');
|
|
199
|
+
for (const key of prefKeys.slice(0, maxEntries)) {
|
|
200
|
+
const pref = this.memory.preferences[key];
|
|
201
|
+
lines.push(` - ${key}: ${pref.value}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return lines.length > 0 ? lines.join('\n') : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async clear() {
|
|
209
|
+
this.memory = this._emptyMemory();
|
|
210
|
+
this.loaded = true;
|
|
211
|
+
await this._save();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const memory = new MemoryStore();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
export class MockReadline extends EventEmitter {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
this.output = {
|
|
7
|
+
write: (data) => {
|
|
8
|
+
// Drop standard writes, as we handle output via React state
|
|
9
|
+
// but AgentLoop calls output.write('\n') around questions.
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
this.pendingQuestion = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
question(prompt, callback) {
|
|
16
|
+
this.pendingQuestion = { prompt, callback };
|
|
17
|
+
this.emit('question', prompt);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
answer(response) {
|
|
21
|
+
if (this.pendingQuestion) {
|
|
22
|
+
const { callback } = this.pendingQuestion;
|
|
23
|
+
this.pendingQuestion = null;
|
|
24
|
+
callback(response);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// OS AI Agent — Dependency Analysis Utilities
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Pure functions for extracting imports and exports from source code.
|
|
5
|
+
// Used by the GET_DEPENDENCIES tool and dependency auto-resolution.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract exported symbol names from JS/TS source content.
|
|
10
|
+
* Handles both ESM (export function/const/class/default/{...})
|
|
11
|
+
* and CommonJS (module.exports = {...}, module.exports.x, exports.x).
|
|
12
|
+
*/
|
|
13
|
+
export function extractExports(content) {
|
|
14
|
+
if (!content) return [];
|
|
15
|
+
const exports = new Set();
|
|
16
|
+
|
|
17
|
+
let m;
|
|
18
|
+
let re = /export\s+(?:async\s+)?function\s*(?:\*\s*)?(\w+)/g;
|
|
19
|
+
while ((m = re.exec(content)) !== null) exports.add(m[1]);
|
|
20
|
+
|
|
21
|
+
re = /export\s+(?:default\s+)?class\s+(\w+)/g;
|
|
22
|
+
while ((m = re.exec(content)) !== null) exports.add(m[1]);
|
|
23
|
+
|
|
24
|
+
re = /export\s+(?:const|let|var)\s+(\w+)/g;
|
|
25
|
+
while ((m = re.exec(content)) !== null) exports.add(m[1]);
|
|
26
|
+
|
|
27
|
+
re = /export\s+default\s+(\w+)/g;
|
|
28
|
+
while ((m = re.exec(content)) !== null) exports.add(m[1]);
|
|
29
|
+
|
|
30
|
+
re = /export\s+\{([^}]+)\}/g;
|
|
31
|
+
while ((m = re.exec(content)) !== null) {
|
|
32
|
+
m[1].split(',').forEach(s => {
|
|
33
|
+
const name = s.trim().match(/^(\w+)/);
|
|
34
|
+
if (name) exports.add(name[1]);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
re = /module\.exports\s*=\s*\{([^}]*)\}/g;
|
|
39
|
+
while ((m = re.exec(content)) !== null) {
|
|
40
|
+
m[1].split(',').forEach(s => {
|
|
41
|
+
const name = s.trim().match(/^(\w+)/);
|
|
42
|
+
if (name) exports.add(name[1]);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
re = /module\.exports\.(\w+)\s*=/g;
|
|
47
|
+
while ((m = re.exec(content)) !== null) exports.add(m[1]);
|
|
48
|
+
|
|
49
|
+
re = /exports\.(\w+)\s*=/g;
|
|
50
|
+
while ((m = re.exec(content)) !== null) exports.add(m[1]);
|
|
51
|
+
|
|
52
|
+
return [...exports];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract local import paths (relative or absolute starting with . or /)
|
|
57
|
+
* from JS/TS source content.
|
|
58
|
+
*/
|
|
59
|
+
export function extractLocalImports(content) {
|
|
60
|
+
if (!content) return [];
|
|
61
|
+
const imports = [];
|
|
62
|
+
const re = /(?:import\s+(?:[\s\S]*?\s+from\s+)?["']([^"']+)["']|require\s*\(\s*["']([^"']+)["']\s*\))/g;
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = re.exec(content)) !== null) {
|
|
65
|
+
const spec = m[1] || m[2];
|
|
66
|
+
if (spec && (spec.startsWith('.') || spec.startsWith('/'))) {
|
|
67
|
+
imports.push(spec);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return imports;
|
|
71
|
+
}
|