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
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
OS AI AGENT
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 OLOJEDE Samuel. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and its source code are proprietary and confidential.
|
|
6
|
+
Unauthorized copying, distribution, modification, or use is strictly
|
|
7
|
+
prohibited without prior written permission from the copyright holder.
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "osai-agent",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OS AI Agent - YOUR AI AGENT",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"osai-agent": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js run",
|
|
12
|
+
"cli": "node src/index.js",
|
|
13
|
+
"run": "node src/index.js run",
|
|
14
|
+
"connect": "node src/index.js connect",
|
|
15
|
+
"dev": "node --watch src/index.js run"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src/",
|
|
19
|
+
"package.json",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/King03-sam/osai-agent.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/King03-sam/osai-agent/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/King03-sam/osai-agent#readme",
|
|
31
|
+
"license": "UNLICENSED",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"cli",
|
|
34
|
+
"agent",
|
|
35
|
+
"ai",
|
|
36
|
+
"sysadmin",
|
|
37
|
+
"network",
|
|
38
|
+
"ssh"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@cfworker/json-schema": "^4.1.1",
|
|
42
|
+
"@modelcontextprotocol/client": "^2.0.0-alpha.2",
|
|
43
|
+
"@tavily/core": "^0.7.3",
|
|
44
|
+
"boxen": "^8.0.1",
|
|
45
|
+
"chalk": "^5.3.0",
|
|
46
|
+
"cli-highlight": "^2.1.11",
|
|
47
|
+
"conf": "^12.0.0",
|
|
48
|
+
"duck-duck-scrape": "^2.2.7",
|
|
49
|
+
"fast-glob": "^3.3.3",
|
|
50
|
+
"@google/generative-ai": "^0.24.1",
|
|
51
|
+
"openai": "^6.42.0",
|
|
52
|
+
"ink": "^7.0.3",
|
|
53
|
+
"ink-scroll-view": "^0.3.7",
|
|
54
|
+
"ink-spinner": "^5.0.0",
|
|
55
|
+
"ink-text-input": "^6.0.0",
|
|
56
|
+
"inquirer": "^13.4.2",
|
|
57
|
+
"marked": "^15.0.12",
|
|
58
|
+
"marked-terminal": "^7.3.0",
|
|
59
|
+
"minimist": "^1.2.8",
|
|
60
|
+
"node-machine-id": "^1.1.12",
|
|
61
|
+
"ora": "^9.4.0",
|
|
62
|
+
"react": "^19.2.6",
|
|
63
|
+
"serpapi": "^2.2.1",
|
|
64
|
+
"ssh2": "^1.15.0",
|
|
65
|
+
"tokenx": "^1.3.0",
|
|
66
|
+
"uuid": "^14.0.0",
|
|
67
|
+
"ws": "^8.18.0"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=18.0.0"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// OS AI Agent — Context Builder
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Builds structured context for the agent loop, including device information,
|
|
5
|
+
// OS detection, and device capabilities. Used primarily in network/SSH mode.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a structured context object for the agent
|
|
10
|
+
*/
|
|
11
|
+
export const buildContext = ({ goal, todos, history, device, mode, step = 0 }) => {
|
|
12
|
+
const context = {
|
|
13
|
+
mode: mode || 'network',
|
|
14
|
+
goal: goal || '',
|
|
15
|
+
todos: todos || [],
|
|
16
|
+
history: history || [],
|
|
17
|
+
step,
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (device) {
|
|
22
|
+
context.device = {
|
|
23
|
+
id: device._id || device.id,
|
|
24
|
+
name: device.name,
|
|
25
|
+
type: device.type,
|
|
26
|
+
ip: device.ip,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (mode === 'network' && device) {
|
|
31
|
+
context.deviceInfo = {
|
|
32
|
+
os: getDeviceOS(device.type),
|
|
33
|
+
capabilities: getDeviceCapabilities(device.type),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return context;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the operating system name for a given device type
|
|
42
|
+
*/
|
|
43
|
+
const getDeviceOS = (deviceType) => {
|
|
44
|
+
const type = (deviceType || '').toLowerCase();
|
|
45
|
+
if (type.includes('cisco')) {
|
|
46
|
+
if (type.includes('asa')) return 'Cisco ASA';
|
|
47
|
+
if (type.includes('ios-xe')) return 'Cisco IOS-XE';
|
|
48
|
+
if (type.includes('nx-os')) return 'Cisco NX-OS';
|
|
49
|
+
return 'Cisco IOS';
|
|
50
|
+
}
|
|
51
|
+
if (type.includes('mikrotik') || type.includes('routeros')) return 'MikroTik RouterOS';
|
|
52
|
+
if (type.includes('pfsense')) return 'pfSense';
|
|
53
|
+
if (type.includes('junos')) return 'Juniper JunOS';
|
|
54
|
+
if (type.includes('linux')) return 'Linux';
|
|
55
|
+
if (type.includes('windows')) return 'Windows Server';
|
|
56
|
+
return 'Unknown';
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get device capabilities based on device type
|
|
61
|
+
*/
|
|
62
|
+
const getDeviceCapabilities = (deviceType) => {
|
|
63
|
+
const type = (deviceType || '').toLowerCase();
|
|
64
|
+
const capabilities = {
|
|
65
|
+
show: true,
|
|
66
|
+
configure: false,
|
|
67
|
+
routing: false,
|
|
68
|
+
firewall: false,
|
|
69
|
+
vlan: false,
|
|
70
|
+
interface: true,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (type.includes('cisco') || type.includes('mikrotik') || type.includes('junos')) {
|
|
74
|
+
capabilities.configure = true;
|
|
75
|
+
capabilities.routing = true;
|
|
76
|
+
capabilities.firewall = true;
|
|
77
|
+
capabilities.vlan = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (type.includes('pfsense')) {
|
|
81
|
+
capabilities.configure = true;
|
|
82
|
+
capabilities.firewall = true;
|
|
83
|
+
capabilities.routing = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (type.includes('linux')) {
|
|
87
|
+
capabilities.configure = true;
|
|
88
|
+
capabilities.routing = true;
|
|
89
|
+
capabilities.firewall = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return capabilities;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Update context with a new result from the agent
|
|
97
|
+
*/
|
|
98
|
+
export const updateContext = (context, { result, command, status }) => {
|
|
99
|
+
return {
|
|
100
|
+
...context,
|
|
101
|
+
history: [
|
|
102
|
+
...context.history,
|
|
103
|
+
{
|
|
104
|
+
step: context.history.length,
|
|
105
|
+
command,
|
|
106
|
+
result,
|
|
107
|
+
status,
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
step: context.step + 1,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if the agent's goal is complete based on context
|
|
117
|
+
*/
|
|
118
|
+
export const isGoalComplete = (context) => {
|
|
119
|
+
if (!context.todos || context.todos.length === 0) {
|
|
120
|
+
return context.history.length > 0;
|
|
121
|
+
}
|
|
122
|
+
return context.todos.every((todo) => todo.completed);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate a device info summary for inclusion in system prompts
|
|
127
|
+
*/
|
|
128
|
+
export const getDeviceInfoSummary = (device) => {
|
|
129
|
+
if (!device) return null;
|
|
130
|
+
|
|
131
|
+
return [
|
|
132
|
+
`Device: ${device.name}`,
|
|
133
|
+
`Type: ${device.type}`,
|
|
134
|
+
`IP: ${device.ip}:${device.port || 22}`,
|
|
135
|
+
`OS: ${getDeviceOS(device.type)}`,
|
|
136
|
+
`Capabilities: ${Object.entries(getDeviceCapabilities(device.type))
|
|
137
|
+
.filter(([_, v]) => v)
|
|
138
|
+
.map(([k]) => k)
|
|
139
|
+
.join(', ')}`,
|
|
140
|
+
].join('\n');
|
|
141
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { TOOLS, CONTEXT_SUMMARY_TAG, HISTORY_SUMMARY_TRIGGER_TOKENS, HISTORY_SUMMARY_KEEP_RECENT_TOKENS, HISTORY_SUMMARY_MIN_GROWTH_TOKENS, MAX_CONTEXT_TOKENS, MAX_CONTEXT_SUMMARY_CHARS } from '../../utils/constants.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
import { estimateTokenCount } from 'tokenx';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
_appendConversationMessage(role, content) {
|
|
7
|
+
if (typeof content !== 'string' || !content.trim()) return;
|
|
8
|
+
this.conversationHistory.push({ role, content });
|
|
9
|
+
this._scheduleBackgroundSummary();
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
_scheduleBackgroundSummary() {
|
|
13
|
+
if (this._summaryInProgress || this._summaryTimer) return;
|
|
14
|
+
const totalTokens = this._estimateTokens();
|
|
15
|
+
if (totalTokens < HISTORY_SUMMARY_TRIGGER_TOKENS) return;
|
|
16
|
+
if ((totalTokens - this._lastSummaryAtTokenCount) < HISTORY_SUMMARY_MIN_GROWTH_TOKENS && this._lastSummaryAtTokenCount > 0) return;
|
|
17
|
+
|
|
18
|
+
const WRITE_TOOLS = new Set([TOOLS.WRITE_FILE, TOOLS.EDIT_FILE, TOOLS.APPEND_FILE]);
|
|
19
|
+
const VALIDATION_TOOLS = new Set([TOOLS.DIAG_POST_EDIT, TOOLS.RUN_SCRIPT, TOOLS.LOCAL_CMD, TOOLS.GIT]);
|
|
20
|
+
const recentTools = this._toolSequenceWindow.map(e => e.tool);
|
|
21
|
+
const lastWriteIdx = [...recentTools].reverse().findIndex(t => WRITE_TOOLS.has(t));
|
|
22
|
+
const lastValidationIdx = [...recentTools].reverse().findIndex(t => VALIDATION_TOOLS.has(t));
|
|
23
|
+
if (lastWriteIdx !== -1 && (lastValidationIdx === -1 || lastWriteIdx < lastValidationIdx)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this._summaryTimer = setTimeout(() => {
|
|
28
|
+
this._summaryTimer = null;
|
|
29
|
+
this._runBackgroundSummary();
|
|
30
|
+
}, 0);
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
_runBackgroundSummary() {
|
|
34
|
+
if (this._summaryInProgress) return;
|
|
35
|
+
this._summaryInProgress = true;
|
|
36
|
+
(async () => {
|
|
37
|
+
try {
|
|
38
|
+
const history = this.conversationHistory
|
|
39
|
+
.filter((m) => m && typeof m.role === 'string' && typeof m.content === 'string')
|
|
40
|
+
.filter((m) => (m.role === 'user' || m.role === 'assistant' || m.role === 'system') && m.content.trim().length > 0);
|
|
41
|
+
|
|
42
|
+
let previousSummary = '';
|
|
43
|
+
let workingHistory = history;
|
|
44
|
+
const first = workingHistory[0];
|
|
45
|
+
if (first?.role === 'system' && first.content.startsWith(CONTEXT_SUMMARY_TAG)) {
|
|
46
|
+
previousSummary = first.content.slice(CONTEXT_SUMMARY_TAG.length).trim();
|
|
47
|
+
workingHistory = workingHistory.slice(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this._estimateTokens(workingHistory) < HISTORY_SUMMARY_TRIGGER_TOKENS) return;
|
|
51
|
+
if (this._estimateTokens(workingHistory) <= HISTORY_SUMMARY_KEEP_RECENT_TOKENS) return;
|
|
52
|
+
|
|
53
|
+
let tokenAccum = 0;
|
|
54
|
+
let cutoff = workingHistory.length;
|
|
55
|
+
for (let i = workingHistory.length - 1; i >= 0; i--) {
|
|
56
|
+
tokenAccum += estimateTokenCount(workingHistory[i].content || '');
|
|
57
|
+
if (tokenAccum > HISTORY_SUMMARY_KEEP_RECENT_TOKENS) {
|
|
58
|
+
cutoff = i + 1;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const oldMessages = workingHistory.slice(0, cutoff);
|
|
63
|
+
const recentMessages = workingHistory.slice(cutoff);
|
|
64
|
+
this.onObservation({
|
|
65
|
+
type: 'context_summary_start',
|
|
66
|
+
totalMessages: workingHistory.length,
|
|
67
|
+
summarizeCount: oldMessages.length,
|
|
68
|
+
keepRecent: HISTORY_SUMMARY_KEEP_RECENT_TOKENS,
|
|
69
|
+
});
|
|
70
|
+
const summaryBody = this._buildContextSummaryBody(oldMessages, previousSummary);
|
|
71
|
+
const summaryMessage = `${CONTEXT_SUMMARY_TAG}\n${summaryBody}`;
|
|
72
|
+
|
|
73
|
+
this.conversationHistory = [{ role: 'system', content: summaryMessage }, ...recentMessages];
|
|
74
|
+
this._lastCompactInteraction = this._toolInteractionCounter;
|
|
75
|
+
this._lastSummaryAtTokenCount = this._estimateTokens();
|
|
76
|
+
this.onObservation({
|
|
77
|
+
type: 'context_summary_end',
|
|
78
|
+
summarizedMessages: oldMessages.length,
|
|
79
|
+
remainingMessages: this.conversationHistory.length,
|
|
80
|
+
});
|
|
81
|
+
this._trimContext();
|
|
82
|
+
this.totalTokensUsed = this._estimateTokens();
|
|
83
|
+
this.onStats?.(this.totalTokensUsed);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.debug('Background summary failed', { error: error.message });
|
|
86
|
+
} finally {
|
|
87
|
+
this._summaryInProgress = false;
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
_buildContextSummaryBody(messages, previousSummary = '') {
|
|
93
|
+
const normalize = (text, max = 180) => String(text || '')
|
|
94
|
+
.replace(/\s+/g, ' ')
|
|
95
|
+
.trim()
|
|
96
|
+
.slice(0, max);
|
|
97
|
+
|
|
98
|
+
const userIntents = [];
|
|
99
|
+
const toolOutcomes = [];
|
|
100
|
+
const assistantNotes = [];
|
|
101
|
+
|
|
102
|
+
for (const msg of messages) {
|
|
103
|
+
const content = String(msg.content || '');
|
|
104
|
+
if (msg.role === 'user' && content.startsWith('[TOOL_RESULT]')) {
|
|
105
|
+
const tool = content.match(/Tool:\s*([A-Z_]+)/i)?.[1] || 'UNKNOWN';
|
|
106
|
+
const target = normalize(content.match(/Target:\s*([^\n]+)/i)?.[1] || '', 120);
|
|
107
|
+
const status = content.match(/Status:\s*([A-Z]+)/i)?.[1] || '';
|
|
108
|
+
const detail = `Tool ${tool}${status ? ` ${status}` : ''}${target ? ` on ${target}` : ''}`;
|
|
109
|
+
toolOutcomes.push(detail);
|
|
110
|
+
|
|
111
|
+
if (tool === 'READ_FILE' && status === 'SUCCESS') {
|
|
112
|
+
const outputMatch = content.match(/Output:\n([\s\S]*)/);
|
|
113
|
+
if (outputMatch) {
|
|
114
|
+
const fileContent = outputMatch[1].trim();
|
|
115
|
+
const maxLines = 80;
|
|
116
|
+
const maxChars = 2000;
|
|
117
|
+
const lines = fileContent.split('\n');
|
|
118
|
+
let preserved = fileContent;
|
|
119
|
+
if (lines.length > maxLines) {
|
|
120
|
+
preserved = lines.slice(0, maxLines).join('\n') + `\n... (${lines.length - maxLines} more lines)`;
|
|
121
|
+
}
|
|
122
|
+
if (preserved.length > maxChars) {
|
|
123
|
+
preserved = preserved.slice(0, maxChars) + `\n... (${fileContent.length - maxChars} more chars)`;
|
|
124
|
+
}
|
|
125
|
+
toolOutcomes.push(`File content (${target}):\n${preserved}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (msg.role === 'user') {
|
|
131
|
+
userIntents.push(normalize(content, 160));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (msg.role === 'assistant') {
|
|
135
|
+
assistantNotes.push(normalize(content, 160));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const takeLast = (arr, n) => arr.filter(Boolean).slice(-n);
|
|
140
|
+
const lines = [];
|
|
141
|
+
lines.push(`Summary refreshed: ${new Date().toISOString()}`);
|
|
142
|
+
|
|
143
|
+
if (previousSummary) {
|
|
144
|
+
lines.push('Previous summary context:');
|
|
145
|
+
lines.push(`- ${normalize(previousSummary, 380)}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const intents = takeLast(userIntents, 5);
|
|
149
|
+
if (intents.length > 0) {
|
|
150
|
+
lines.push('User intents:');
|
|
151
|
+
for (const item of intents) lines.push(`- ${item}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const outcomes = takeLast(toolOutcomes, 8);
|
|
155
|
+
if (outcomes.length > 0) {
|
|
156
|
+
lines.push('Tool outcomes:');
|
|
157
|
+
for (const item of outcomes) lines.push(`- ${item}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const notes = takeLast(assistantNotes, 5);
|
|
161
|
+
if (notes.length > 0) {
|
|
162
|
+
lines.push('Assistant conclusions:');
|
|
163
|
+
for (const item of notes) lines.push(`- ${item}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const summary = lines.join('\n').trim();
|
|
167
|
+
if (summary.length <= MAX_CONTEXT_SUMMARY_CHARS) return summary;
|
|
168
|
+
return summary.slice(0, MAX_CONTEXT_SUMMARY_CHARS) + '\n...[summary truncated]';
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
_trimContext() {
|
|
172
|
+
this._scheduleBackgroundSummary();
|
|
173
|
+
if (this._estimateTokens() <= MAX_CONTEXT_TOKENS) return;
|
|
174
|
+
let tokenAccum = 0;
|
|
175
|
+
let cutoff = this.conversationHistory.length;
|
|
176
|
+
for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
|
|
177
|
+
tokenAccum += estimateTokenCount(this.conversationHistory[i].content || '');
|
|
178
|
+
if (tokenAccum > MAX_CONTEXT_TOKENS) {
|
|
179
|
+
cutoff = i + 1;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const first = this.conversationHistory[0];
|
|
184
|
+
if (first?.role === 'system') {
|
|
185
|
+
const rest = this.conversationHistory.slice(cutoff);
|
|
186
|
+
this.conversationHistory = [first, ...rest];
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.conversationHistory = this.conversationHistory.slice(cutoff);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
_estimateTokens(messages) {
|
|
193
|
+
const target = messages || this.conversationHistory;
|
|
194
|
+
return target.reduce((sum, msg) => sum + estimateTokenCount(msg.content || ''), 0);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { TOOLS, MODES } from '../../utils/constants.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
_isPathInsideCodingRoot(candidatePath) {
|
|
7
|
+
if (!candidatePath || !this._codingRootPath) return true;
|
|
8
|
+
if (candidatePath === this._codingRootPath) return true;
|
|
9
|
+
return candidatePath.startsWith(this._codingRootPath + path.sep);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
_getDirectoryCandidate(candidatePath) {
|
|
13
|
+
if (!candidatePath) return null;
|
|
14
|
+
try {
|
|
15
|
+
const st = fs.existsSync(candidatePath) ? fs.statSync(candidatePath) : null;
|
|
16
|
+
if (st?.isDirectory()) return candidatePath;
|
|
17
|
+
return path.dirname(candidatePath);
|
|
18
|
+
} catch {
|
|
19
|
+
return path.dirname(candidatePath);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
_isApprovedExternalDir(dirPath) {
|
|
24
|
+
for (const approved of this._approvedExternalDirs) {
|
|
25
|
+
if (dirPath === approved || dirPath.startsWith(approved + path.sep)) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
_extractCommandDirectoryTargets(cmdRaw) {
|
|
31
|
+
const cmd = String(cmdRaw || '');
|
|
32
|
+
if (!cmd.trim()) return [];
|
|
33
|
+
const targets = [];
|
|
34
|
+
const segments = cmd.split(/&&|\|\||;/g).map((s) => s.trim()).filter(Boolean);
|
|
35
|
+
for (const seg of segments) {
|
|
36
|
+
let m = seg.match(/^cd(?:\s+\/d)?\s+(.+)$/i);
|
|
37
|
+
if (!m) m = seg.match(/^(?:Set-Location|sl)\s+(.+)$/i);
|
|
38
|
+
if (!m) continue;
|
|
39
|
+
let target = String(m[1] || '').trim();
|
|
40
|
+
if (!target) continue;
|
|
41
|
+
target = target.replace(/^["'`]+|["'`]+$/g, '');
|
|
42
|
+
if (!target || target === '.') continue;
|
|
43
|
+
targets.push(target);
|
|
44
|
+
}
|
|
45
|
+
return targets;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
_getDirectoryApprovalRequirement(toolCall) {
|
|
49
|
+
if (this.mode !== MODES.CODING) return { required: false };
|
|
50
|
+
if (this.isSubagent) return { required: false };
|
|
51
|
+
const tool = toolCall.tool || TOOLS.LOCAL_CMD;
|
|
52
|
+
const rawCandidates = [];
|
|
53
|
+
|
|
54
|
+
const pushPath = (v) => { if (typeof v === 'string' && v.trim()) rawCandidates.push(v.trim()); };
|
|
55
|
+
|
|
56
|
+
switch (tool) {
|
|
57
|
+
case TOOLS.READ_FILE:
|
|
58
|
+
case TOOLS.WRITE_FILE:
|
|
59
|
+
case TOOLS.EDIT_FILE:
|
|
60
|
+
case TOOLS.APPEND_FILE:
|
|
61
|
+
case TOOLS.DELETE_FILE:
|
|
62
|
+
case TOOLS.LIST_DIR:
|
|
63
|
+
case TOOLS.SEARCH_FILE:
|
|
64
|
+
case TOOLS.CREATE_DIR:
|
|
65
|
+
case TOOLS.TREE_VIEW:
|
|
66
|
+
case TOOLS.RUN_SCRIPT:
|
|
67
|
+
case TOOLS.FILE_INFO:
|
|
68
|
+
case TOOLS.GREP:
|
|
69
|
+
case TOOLS.GIT:
|
|
70
|
+
pushPath(toolCall.path);
|
|
71
|
+
pushPath(toolCall.cwd);
|
|
72
|
+
break;
|
|
73
|
+
case TOOLS.MOVE_FILE:
|
|
74
|
+
case TOOLS.COPY_FILE:
|
|
75
|
+
pushPath(toolCall.source);
|
|
76
|
+
pushPath(toolCall.destination);
|
|
77
|
+
break;
|
|
78
|
+
case TOOLS.LOCAL_CMD: {
|
|
79
|
+
const cmdTargets = this._extractCommandDirectoryTargets(toolCall.cmd);
|
|
80
|
+
for (const t of cmdTargets) rawCandidates.push(t);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const raw of rawCandidates) {
|
|
88
|
+
const normalized = this._normalizeTrackedPath(raw);
|
|
89
|
+
if (!normalized) continue;
|
|
90
|
+
const dirCandidate = this._getDirectoryCandidate(normalized) || normalized;
|
|
91
|
+
if (this._isPathInsideCodingRoot(dirCandidate)) continue;
|
|
92
|
+
if (this._isApprovedExternalDir(dirCandidate)) continue;
|
|
93
|
+
return {
|
|
94
|
+
required: true,
|
|
95
|
+
targetPath: dirCandidate,
|
|
96
|
+
reason: `Target is outside current coding directory (${this._codingRootPath}).`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { required: false };
|
|
101
|
+
},
|
|
102
|
+
};
|