mocode-ai 1.1.7 → 1.1.8
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/README.md +23 -37
- package/README.zh-CN.md +46 -38
- package/dist/agent/core.js +100 -446
- package/dist/agent/index.js +2 -21
- package/dist/agent/spawn.js +3 -5
- package/dist/agent/work-discipline.js +16 -70
- package/dist/config/index.js +6 -7
- package/dist/context/age-aware.js +18 -48
- package/dist/context/artifacts.js +19 -17
- package/dist/context/budget.js +27 -28
- package/dist/context/classifier.js +0 -1
- package/dist/context/encoders/index.js +4 -11
- package/dist/context/index.js +4 -7
- package/dist/context/lifecycle.js +115 -483
- package/dist/context/pipeline.js +8 -15
- package/dist/context/relevance.js +77 -55
- package/dist/host/stdio.js +0 -6
- package/dist/i18n/index.js +0 -6
- package/dist/index.js +11 -1
- package/dist/llm/index.js +72 -3
- package/dist/mcp/index.js +0 -1
- package/dist/repl/index.js +21 -15
- package/dist/runtime/browser-manager.js +299 -0
- package/dist/runtime/dev-server-manager.js +354 -0
- package/dist/runtime/shutdown.js +26 -0
- package/dist/session/compact.js +86 -102
- package/dist/session/index.js +0 -1
- package/dist/session/scheduler.js +88 -92
- package/dist/session/trace-metrics.js +5 -92
- package/dist/session/trace.js +1 -10
- package/dist/tools/builtins/browser.js +199 -0
- package/dist/tools/builtins/dev-server.js +99 -0
- package/dist/tools/builtins/index.js +28 -19
- package/dist/tools/builtins/screenshot.js +173 -0
- package/dist/tools/builtins/view-image.js +49 -0
- package/dist/tools/constants.js +3 -0
- package/dist/tools/registry.js +5 -29
- package/dist/ui/layout.js +28 -5
- package/dist/ui/render.js +11 -0
- package/package.json +2 -2
- package/dist/agent/middleware/checklist.js +0 -59
- package/dist/session/drop.d.ts +0 -19
- package/dist/session/drop.js +0 -93
- package/dist/tools/builtins/drop-context.d.ts +0 -18
- package/dist/tools/builtins/drop-context.js +0 -68
- package/dist/verification/diagnostics.js +0 -108
- package/dist/verification/fingerprint.js +0 -54
- package/dist/verification/index.js +0 -333
- package/dist/verification/postconditions.js +0 -98
- package/dist/verification/targeted-tests.js +0 -96
- package/dist/verification/types.js +0 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, extname } from 'node:path';
|
|
3
|
+
import { jailResolve } from '../../sandbox/index.js';
|
|
4
|
+
import { MAX_INLINE_BYTES_DEFAULT } from '../../attachments/image.js';
|
|
5
|
+
import { BrowserManagerError, click, closeAllBrowsers, closeSession, fill, listSessions, navigate, openSession, press, readText, screenshot, waitFor, } from '../../runtime/browser-manager.js';
|
|
6
|
+
function result(status, code, payload) {
|
|
7
|
+
return { status, code, retryable: false, output: JSON.stringify(payload, null, 2) };
|
|
8
|
+
}
|
|
9
|
+
function invalid(message) {
|
|
10
|
+
return result('error', 'INVALID_ARGUMENTS', { error: message });
|
|
11
|
+
}
|
|
12
|
+
function optionalString(args, name) {
|
|
13
|
+
const value = args[name];
|
|
14
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
15
|
+
}
|
|
16
|
+
function failure(error) {
|
|
17
|
+
if (error instanceof BrowserManagerError) {
|
|
18
|
+
const status = error.code === 'SANDBOX_DENIED' ? 'denied' : 'error';
|
|
19
|
+
return result(status, error.code, { error: error.message });
|
|
20
|
+
}
|
|
21
|
+
return result('error', 'EXECUTION_ERROR', {
|
|
22
|
+
error: error instanceof Error ? error.message : String(error),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export const browserTool = {
|
|
26
|
+
name: 'browser',
|
|
27
|
+
description: 'Drive a real Chromium browser to inspect a running frontend. Sessions persist across tool calls, ' +
|
|
28
|
+
'so open once and then navigate, interact, and screenshot the same page.\n' +
|
|
29
|
+
'Actions: open, navigate, click, fill, press, wait_for, text, screenshot, console, list, close.\n' +
|
|
30
|
+
'screenshot returns the rendered page as visual input, so you can actually see layout and styling issues. ' +
|
|
31
|
+
'Every result also reports recent console messages, page errors, and failed requests.\n' +
|
|
32
|
+
'Start the app with dev_server first. Only localhost URLs are allowed by default.',
|
|
33
|
+
risk: 'confirm',
|
|
34
|
+
parameters: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
action: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
enum: ['open', 'navigate', 'click', 'fill', 'press', 'wait_for', 'text', 'screenshot', 'console', 'list', 'close'],
|
|
40
|
+
description: 'Operation to perform',
|
|
41
|
+
},
|
|
42
|
+
sessionId: { type: 'string', description: 'Session from action=open; optional when only one session exists' },
|
|
43
|
+
url: { type: 'string', description: 'Target URL for action=navigate, e.g. http://localhost:5173' },
|
|
44
|
+
selector: { type: 'string', description: 'CSS selector for click/fill/press/wait_for/text/screenshot' },
|
|
45
|
+
value: { type: 'string', description: 'Text to type for action=fill' },
|
|
46
|
+
key: { type: 'string', description: 'Key for action=press, e.g. Enter' },
|
|
47
|
+
fullPage: { type: 'boolean', description: 'Capture the entire scrollable page (action=screenshot)' },
|
|
48
|
+
headed: { type: 'boolean', description: 'Show a visible browser window on action=open (default: headless)' },
|
|
49
|
+
path: { type: 'string', description: 'Optional PNG path inside the workspace to also save a screenshot' },
|
|
50
|
+
detail: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
enum: ['auto', 'low', 'high'],
|
|
53
|
+
description: 'Vision detail level for screenshots (default: high)',
|
|
54
|
+
},
|
|
55
|
+
timeoutMs: { type: 'integer', description: 'Action timeout in ms (default 15000, max 60000)' },
|
|
56
|
+
limit: { type: 'integer', description: 'Max characters for action=text (default 4000)' },
|
|
57
|
+
},
|
|
58
|
+
required: ['action'],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
async execute(args) {
|
|
62
|
+
const action = String(args.action);
|
|
63
|
+
const sessionId = optionalString(args, 'sessionId');
|
|
64
|
+
try {
|
|
65
|
+
switch (action) {
|
|
66
|
+
case 'open':
|
|
67
|
+
return result('success', 'OK', await openSession({ headed: args.headed === true }));
|
|
68
|
+
case 'list':
|
|
69
|
+
return result('success', 'OK', { sessions: listSessions() });
|
|
70
|
+
case 'navigate': {
|
|
71
|
+
const url = optionalString(args, 'url');
|
|
72
|
+
if (!url)
|
|
73
|
+
return invalid('action=navigate requires a url.');
|
|
74
|
+
return result('success', 'OK', await navigate({
|
|
75
|
+
...(sessionId ? { sessionId } : {}),
|
|
76
|
+
url,
|
|
77
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }),
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
case 'click':
|
|
81
|
+
case 'wait_for': {
|
|
82
|
+
const selector = optionalString(args, 'selector');
|
|
83
|
+
if (!selector)
|
|
84
|
+
return invalid(`action=${action} requires a selector.`);
|
|
85
|
+
const opts = {
|
|
86
|
+
...(sessionId ? { sessionId } : {}),
|
|
87
|
+
selector,
|
|
88
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }),
|
|
89
|
+
};
|
|
90
|
+
return result('success', 'OK', action === 'click' ? await click(opts) : await waitFor(opts));
|
|
91
|
+
}
|
|
92
|
+
case 'fill': {
|
|
93
|
+
const selector = optionalString(args, 'selector');
|
|
94
|
+
if (!selector)
|
|
95
|
+
return invalid('action=fill requires a selector.');
|
|
96
|
+
if (typeof args.value !== 'string')
|
|
97
|
+
return invalid('action=fill requires a value string.');
|
|
98
|
+
return result('success', 'OK', await fill({
|
|
99
|
+
...(sessionId ? { sessionId } : {}),
|
|
100
|
+
selector,
|
|
101
|
+
value: args.value,
|
|
102
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }),
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
case 'press': {
|
|
106
|
+
const key = optionalString(args, 'key');
|
|
107
|
+
if (!key)
|
|
108
|
+
return invalid('action=press requires a key.');
|
|
109
|
+
return result('success', 'OK', await press({
|
|
110
|
+
...(sessionId ? { sessionId } : {}),
|
|
111
|
+
key,
|
|
112
|
+
...(optionalString(args, 'selector') ? { selector: optionalString(args, 'selector') } : {}),
|
|
113
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
case 'text':
|
|
117
|
+
return result('success', 'OK', await readText({
|
|
118
|
+
...(sessionId ? { sessionId } : {}),
|
|
119
|
+
...(optionalString(args, 'selector') ? { selector: optionalString(args, 'selector') } : {}),
|
|
120
|
+
...(args.limit === undefined ? {} : { limit: Number(args.limit) }),
|
|
121
|
+
}));
|
|
122
|
+
case 'console': {
|
|
123
|
+
const sessions = listSessions();
|
|
124
|
+
const shot = await readText({ ...(sessionId ? { sessionId } : {}), limit: 100 });
|
|
125
|
+
return result('success', 'OK', {
|
|
126
|
+
sessionId: shot.sessionId,
|
|
127
|
+
url: shot.url,
|
|
128
|
+
diagnostics: shot.diagnostics,
|
|
129
|
+
openSessions: sessions.length,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
case 'screenshot': {
|
|
133
|
+
const captured = await screenshot({
|
|
134
|
+
...(sessionId ? { sessionId } : {}),
|
|
135
|
+
fullPage: args.fullPage === true,
|
|
136
|
+
...(optionalString(args, 'selector') ? { selector: optionalString(args, 'selector') } : {}),
|
|
137
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }),
|
|
138
|
+
});
|
|
139
|
+
const { buffer, ...meta } = captured;
|
|
140
|
+
if (buffer.byteLength > MAX_INLINE_BYTES_DEFAULT) {
|
|
141
|
+
return result('error', 'EXECUTION_ERROR', {
|
|
142
|
+
error: `Screenshot is ${buffer.byteLength} bytes, above the ${MAX_INLINE_BYTES_DEFAULT} byte inline limit. Retry with fullPage=false or a narrower selector.`,
|
|
143
|
+
...meta,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
let savedPath;
|
|
147
|
+
const requestedPath = optionalString(args, 'path');
|
|
148
|
+
if (requestedPath) {
|
|
149
|
+
if (extname(requestedPath).toLowerCase() !== '.png') {
|
|
150
|
+
return invalid('path must use the .png extension.');
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const absolute = jailResolve(requestedPath);
|
|
154
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
155
|
+
await writeFile(absolute, buffer);
|
|
156
|
+
savedPath = requestedPath;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return result('denied', 'SANDBOX_DENIED', {
|
|
160
|
+
error: error instanceof Error ? error.message : String(error),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const detail = args.detail === 'low' || args.detail === 'auto' ? args.detail : 'high';
|
|
165
|
+
return {
|
|
166
|
+
status: 'success',
|
|
167
|
+
code: 'OK',
|
|
168
|
+
retryable: false,
|
|
169
|
+
output: JSON.stringify({
|
|
170
|
+
...meta,
|
|
171
|
+
bytes: buffer.byteLength,
|
|
172
|
+
...(savedPath ? { savedPath } : {}),
|
|
173
|
+
note: 'Rendered page is attached to the next model request.',
|
|
174
|
+
}, null, 2),
|
|
175
|
+
modelAttachments: [{
|
|
176
|
+
type: 'image',
|
|
177
|
+
name: `${meta.sessionId}.png`,
|
|
178
|
+
mime: 'image/png',
|
|
179
|
+
dataUrl: `data:image/png;base64,${buffer.toString('base64')}`,
|
|
180
|
+
detail,
|
|
181
|
+
}],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
case 'close': {
|
|
185
|
+
if (!sessionId && listSessions().length > 1) {
|
|
186
|
+
await closeAllBrowsers();
|
|
187
|
+
return result('success', 'OK', { closed: 'all' });
|
|
188
|
+
}
|
|
189
|
+
return result('success', 'OK', await closeSession(sessionId));
|
|
190
|
+
}
|
|
191
|
+
default:
|
|
192
|
+
return invalid(`Unsupported action: ${action}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return failure(error);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { DevServerManagerError, getDevServerStatus, readDevServerLogs, startDevServer, stopDevServer, } from '../../runtime/dev-server-manager.js';
|
|
2
|
+
function result(status, code, payload) {
|
|
3
|
+
return { status, code, retryable: false, output: JSON.stringify(payload, null, 2) };
|
|
4
|
+
}
|
|
5
|
+
function invalid(message) {
|
|
6
|
+
return result('error', 'INVALID_ARGUMENTS', { error: message });
|
|
7
|
+
}
|
|
8
|
+
function optionalString(args, name) {
|
|
9
|
+
const value = args[name];
|
|
10
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
11
|
+
}
|
|
12
|
+
function failure(error) {
|
|
13
|
+
if (error instanceof DevServerManagerError) {
|
|
14
|
+
const status = error.code === 'SANDBOX_DENIED'
|
|
15
|
+
? 'denied'
|
|
16
|
+
: error.code === 'ABORTED' ? 'aborted' : 'error';
|
|
17
|
+
return result(status, error.code, { error: error.message });
|
|
18
|
+
}
|
|
19
|
+
return result('error', 'EXECUTION_ERROR', {
|
|
20
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export const devServerTool = {
|
|
24
|
+
name: 'dev_server',
|
|
25
|
+
description: 'Manage long-running background processes such as a frontend dev server. Unlike run_command, ' +
|
|
26
|
+
'the process keeps running across tool calls, so you can start a server, then inspect it with the browser tool.\n' +
|
|
27
|
+
'Actions: start (needs command; optional readyUrl or readyPattern to wait until it actually serves), ' +
|
|
28
|
+
'status (all servers, or one by id), logs (tail output; pass offset for incremental reads), stop (terminate the process tree).\n' +
|
|
29
|
+
'Always stop servers you started once you are done verifying.',
|
|
30
|
+
risk: 'dangerous',
|
|
31
|
+
parameters: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
action: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
enum: ['start', 'status', 'logs', 'stop'],
|
|
37
|
+
description: 'Operation to perform',
|
|
38
|
+
},
|
|
39
|
+
id: { type: 'string', description: 'Server id returned by start (required for logs/stop)' },
|
|
40
|
+
command: { type: 'string', description: 'Command to run for action=start, e.g. "npm run dev"' },
|
|
41
|
+
cwd: { type: 'string', description: 'Working directory inside the workspace (default: workspace root)' },
|
|
42
|
+
readyUrl: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Wait until this loopback URL responds, e.g. http://localhost:5173. Only localhost/127.0.0.1/::1 allowed.',
|
|
45
|
+
},
|
|
46
|
+
readyPattern: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'Wait until server output matches this case-insensitive regex, e.g. "ready in|listening on"',
|
|
49
|
+
},
|
|
50
|
+
timeoutMs: { type: 'integer', description: 'Readiness wait budget in ms (default 30000, max 180000)' },
|
|
51
|
+
offset: { type: 'integer', description: 'Byte offset for action=logs (default: tail)' },
|
|
52
|
+
limit: { type: 'integer', description: 'Max bytes for action=logs (default 8000, max 64000)' },
|
|
53
|
+
},
|
|
54
|
+
required: ['action'],
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
},
|
|
57
|
+
async execute(args, ctx) {
|
|
58
|
+
const action = String(args.action);
|
|
59
|
+
try {
|
|
60
|
+
if (action === 'start') {
|
|
61
|
+
const command = optionalString(args, 'command');
|
|
62
|
+
if (!command)
|
|
63
|
+
return invalid('action=start requires a non-empty command.');
|
|
64
|
+
const started = await startDevServer({
|
|
65
|
+
command,
|
|
66
|
+
cwd: optionalString(args, 'cwd'),
|
|
67
|
+
readyUrl: optionalString(args, 'readyUrl'),
|
|
68
|
+
readyPattern: optionalString(args, 'readyPattern'),
|
|
69
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }),
|
|
70
|
+
...(ctx?.signal ? { signal: ctx.signal } : {}),
|
|
71
|
+
});
|
|
72
|
+
return result('success', 'OK', started);
|
|
73
|
+
}
|
|
74
|
+
if (action === 'status') {
|
|
75
|
+
return result('success', 'OK', { servers: getDevServerStatus(optionalString(args, 'id')) });
|
|
76
|
+
}
|
|
77
|
+
if (action === 'logs') {
|
|
78
|
+
const id = optionalString(args, 'id');
|
|
79
|
+
if (!id)
|
|
80
|
+
return invalid('action=logs requires the server id.');
|
|
81
|
+
const logs = await readDevServerLogs(id, {
|
|
82
|
+
...(args.offset === undefined ? {} : { offset: Number(args.offset) }),
|
|
83
|
+
...(args.limit === undefined ? {} : { limit: Number(args.limit) }),
|
|
84
|
+
});
|
|
85
|
+
return result('success', 'OK', logs);
|
|
86
|
+
}
|
|
87
|
+
if (action === 'stop') {
|
|
88
|
+
const id = optionalString(args, 'id');
|
|
89
|
+
if (!id)
|
|
90
|
+
return invalid('action=stop requires the server id.');
|
|
91
|
+
return result('success', 'OK', await stopDevServer(id));
|
|
92
|
+
}
|
|
93
|
+
return invalid(`Unsupported action: ${action}`);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return failure(error);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { readFileTool } from './read-file.js';
|
|
2
|
+
import { viewImageTool } from './view-image.js';
|
|
3
|
+
import { screenshotTool } from './screenshot.js';
|
|
2
4
|
import { writeFileTool } from './write-file.js';
|
|
3
5
|
import { editFileTool } from './edit-file.js';
|
|
4
6
|
import { runCommandTool } from './run-command.js';
|
|
7
|
+
import { devServerTool } from './dev-server.js';
|
|
8
|
+
import { browserTool } from './browser.js';
|
|
5
9
|
import { globTool } from './glob.js';
|
|
6
10
|
import { grepTool } from './grep.js';
|
|
7
11
|
import { webSearchTool } from './web-search.js';
|
|
8
12
|
import { webFetchTool } from './web-fetch.js';
|
|
9
13
|
import { useSkillTool } from './use-skill.js';
|
|
10
14
|
import { askHumanTool } from './ask-human.js';
|
|
11
|
-
import { dropContextTool } from './drop-context.js';
|
|
12
15
|
import { memorySaveTool } from './memory-save.js';
|
|
13
16
|
import { memorySearchTool } from './memory-search.js';
|
|
14
17
|
import { memoryListTool } from './memory-list.js';
|
|
@@ -42,27 +45,30 @@ const pathResource = (args) => typeof args.path === 'string' && args.path ? [`fi
|
|
|
42
45
|
const workspaceResource = () => ['workspace'];
|
|
43
46
|
const memoryResource = () => ['memory-store'];
|
|
44
47
|
const CAPABILITIES = {
|
|
45
|
-
read_file: { effect: 'read', concurrency: 'parallel',
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
read_file: { effect: 'read', concurrency: 'parallel', resources: pathResource },
|
|
49
|
+
view_image: { effect: 'read', concurrency: 'parallel', resources: pathResource },
|
|
50
|
+
screenshot: { effect: 'process', concurrency: 'serial', resources: workspaceResource, supportsAbort: true },
|
|
51
|
+
write_file: { effect: 'write', concurrency: 'resource-locked', resources: pathResource, delegatesResourceLocks: true },
|
|
52
|
+
edit_file: { effect: 'write', concurrency: 'resource-locked', resources: pathResource, delegatesResourceLocks: true },
|
|
53
|
+
run_command: { effect: 'process', concurrency: 'serial', resources: workspaceResource, supportsAbort: true },
|
|
54
|
+
// 后台进程与浏览器会话跨调用存活;串行执行避免同一页面/服务被并发操作。
|
|
55
|
+
dev_server: { effect: 'process', concurrency: 'serial', resources: workspaceResource, supportsAbort: true },
|
|
56
|
+
browser: { effect: 'process', concurrency: 'serial', resources: workspaceResource },
|
|
57
|
+
glob: { effect: 'read', concurrency: 'parallel', resources: workspaceResource },
|
|
58
|
+
grep: { effect: 'read', concurrency: 'parallel', resources: workspaceResource },
|
|
59
|
+
web_search: { effect: 'network', concurrency: 'parallel', supportsAbort: true },
|
|
60
|
+
web_fetch: { effect: 'network', concurrency: 'parallel', supportsAbort: true },
|
|
61
|
+
use_skill: { effect: 'read', concurrency: 'serial' },
|
|
62
|
+
ask_human: { effect: 'read', concurrency: 'serial' },
|
|
63
|
+
memory_save: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
64
|
+
memory_search: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
65
|
+
memory_list: { effect: 'read', concurrency: 'serial', resources: memoryResource },
|
|
66
|
+
memory_update: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
67
|
+
memory_forget: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
61
68
|
// sub-agent 动态协调:只读任务无锁并行;写任务在 overlay 中执行,merge 时由 ChangeSet 持 canonical lock。
|
|
62
69
|
'sub-agent': {
|
|
63
70
|
effect: 'write',
|
|
64
71
|
concurrency: 'resource-locked',
|
|
65
|
-
retry: 'never',
|
|
66
72
|
resources: (args) => args.mode === 'write' && Array.isArray(args.writeSet) && args.writeSet.length
|
|
67
73
|
? args.writeSet.map((item) => `file:${String(item)}`)
|
|
68
74
|
: args.mode === 'write' ? ['workspace'] : [],
|
|
@@ -72,16 +78,19 @@ const CAPABILITIES = {
|
|
|
72
78
|
};
|
|
73
79
|
const rawBuiltinTools = [
|
|
74
80
|
readFileTool,
|
|
81
|
+
viewImageTool,
|
|
82
|
+
screenshotTool,
|
|
75
83
|
writeFileTool,
|
|
76
84
|
editFileTool,
|
|
77
85
|
runCommandTool,
|
|
86
|
+
devServerTool,
|
|
87
|
+
browserTool,
|
|
78
88
|
globTool,
|
|
79
89
|
grepTool,
|
|
80
90
|
webSearchTool,
|
|
81
91
|
webFetchTool,
|
|
82
92
|
useSkillTool,
|
|
83
93
|
askHumanTool,
|
|
84
|
-
dropContextTool,
|
|
85
94
|
..._memoryTools,
|
|
86
95
|
subAgentTool,
|
|
87
96
|
];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { dirname, extname } from 'node:path';
|
|
4
|
+
import { loadImageAttachment, MAX_INLINE_BYTES_DEFAULT, } from '../../attachments/image.js';
|
|
5
|
+
import { filterEnv, jailResolve } from '../../sandbox/index.js';
|
|
6
|
+
function runCaptureProcess(program, args, env, signal) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const child = spawn(program, args, {
|
|
9
|
+
env,
|
|
10
|
+
windowsHide: true,
|
|
11
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
12
|
+
});
|
|
13
|
+
let stderr = '';
|
|
14
|
+
let finished = false;
|
|
15
|
+
const finish = (result) => {
|
|
16
|
+
if (finished)
|
|
17
|
+
return;
|
|
18
|
+
finished = true;
|
|
19
|
+
signal?.removeEventListener('abort', onAbort);
|
|
20
|
+
resolve(result);
|
|
21
|
+
};
|
|
22
|
+
const onAbort = () => {
|
|
23
|
+
child.kill();
|
|
24
|
+
finish({ status: 'aborted', detail: 'Screenshot capture was aborted.' });
|
|
25
|
+
};
|
|
26
|
+
child.stderr.on('data', (chunk) => {
|
|
27
|
+
stderr = (stderr + chunk.toString('utf8')).slice(-4000);
|
|
28
|
+
});
|
|
29
|
+
child.on('error', (error) => finish({ status: 'spawn_error', detail: error.message }));
|
|
30
|
+
child.on('close', (code) => finish({
|
|
31
|
+
status: code === 0 ? 'passed' : 'failed',
|
|
32
|
+
detail: stderr.trim() || `Capture process exited with code ${code ?? 'null'}.`,
|
|
33
|
+
}));
|
|
34
|
+
if (signal) {
|
|
35
|
+
if (signal.aborted)
|
|
36
|
+
onAbort();
|
|
37
|
+
else
|
|
38
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const WINDOWS_CAPTURE_SCRIPT = [
|
|
43
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
44
|
+
'Add-Type -AssemblyName System.Drawing',
|
|
45
|
+
"$bounds = if ($env:MOCODE_SCREENSHOT_TARGET -eq 'all') { [System.Windows.Forms.SystemInformation]::VirtualScreen } else { [System.Windows.Forms.Screen]::PrimaryScreen.Bounds }",
|
|
46
|
+
'$bitmap = [System.Drawing.Bitmap]::new($bounds.Width, $bounds.Height)',
|
|
47
|
+
'$graphics = [System.Drawing.Graphics]::FromImage($bitmap)',
|
|
48
|
+
'try {',
|
|
49
|
+
' $graphics.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size)',
|
|
50
|
+
' $bitmap.Save($env:MOCODE_SCREENSHOT_PATH, [System.Drawing.Imaging.ImageFormat]::Png)',
|
|
51
|
+
'} finally {',
|
|
52
|
+
' $graphics.Dispose()',
|
|
53
|
+
' $bitmap.Dispose()',
|
|
54
|
+
'}',
|
|
55
|
+
].join('\n');
|
|
56
|
+
async function captureDesktop(outputPath, target, signal) {
|
|
57
|
+
const env = {
|
|
58
|
+
...filterEnv(process.env),
|
|
59
|
+
MOCODE_SCREENSHOT_PATH: outputPath,
|
|
60
|
+
MOCODE_SCREENSHOT_TARGET: target,
|
|
61
|
+
};
|
|
62
|
+
if (process.platform === 'win32') {
|
|
63
|
+
return runCaptureProcess('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', WINDOWS_CAPTURE_SCRIPT], env, signal);
|
|
64
|
+
}
|
|
65
|
+
if (process.platform === 'darwin') {
|
|
66
|
+
const args = ['-x', ...(target === 'primary' ? ['-m'] : []), outputPath];
|
|
67
|
+
return runCaptureProcess('/usr/sbin/screencapture', args, env, signal);
|
|
68
|
+
}
|
|
69
|
+
const candidates = [
|
|
70
|
+
['gnome-screenshot', ['-f', outputPath]],
|
|
71
|
+
['grim', [outputPath]],
|
|
72
|
+
['scrot', [outputPath]],
|
|
73
|
+
['import', ['-window', 'root', outputPath]],
|
|
74
|
+
];
|
|
75
|
+
const failures = [];
|
|
76
|
+
for (const [program, args] of candidates) {
|
|
77
|
+
const result = await runCaptureProcess(program, args, env, signal);
|
|
78
|
+
if (result.status === 'passed' || result.status === 'aborted')
|
|
79
|
+
return result;
|
|
80
|
+
failures.push(`${program}: ${result.detail}`);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
status: 'failed',
|
|
84
|
+
detail: `No supported Linux screenshot command succeeded. ${failures.join(' | ')}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export const screenshotTool = {
|
|
88
|
+
name: 'screenshot',
|
|
89
|
+
description: 'Capture the desktop and inspect it as visual model input. Use this to diagnose visible UI state, dialogs, rendering problems, or applications that cannot be understood from source files alone. The user must approve each capture because screenshots may contain sensitive information.',
|
|
90
|
+
risk: 'confirm',
|
|
91
|
+
parameters: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
path: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'Optional output PNG path inside the workspace. Defaults to .mocode/screenshots/<timestamp>.png.',
|
|
97
|
+
},
|
|
98
|
+
target: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
enum: ['primary', 'all'],
|
|
101
|
+
description: 'Capture the primary display or the full virtual desktop (default: primary).',
|
|
102
|
+
},
|
|
103
|
+
detail: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
enum: ['auto', 'low', 'high'],
|
|
106
|
+
description: 'Vision detail level (default: high).',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
},
|
|
111
|
+
async execute(args, ctx) {
|
|
112
|
+
const defaultName = `.mocode/screenshots/screenshot-${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
|
|
113
|
+
let requestedPath = String(args.path ?? defaultName).trim();
|
|
114
|
+
if (!requestedPath)
|
|
115
|
+
requestedPath = defaultName;
|
|
116
|
+
if (!extname(requestedPath))
|
|
117
|
+
requestedPath += '.png';
|
|
118
|
+
if (extname(requestedPath).toLowerCase() !== '.png') {
|
|
119
|
+
return {
|
|
120
|
+
status: 'error', code: 'INVALID_ARGUMENTS', retryable: false,
|
|
121
|
+
output: 'Screenshot output path must use the .png extension.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
let outputPath;
|
|
125
|
+
try {
|
|
126
|
+
outputPath = jailResolve(requestedPath);
|
|
127
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
return { status: 'denied', code: 'SANDBOX_DENIED', retryable: false, output: message };
|
|
132
|
+
}
|
|
133
|
+
const target = args.target === 'all' ? 'all' : 'primary';
|
|
134
|
+
const capture = await captureDesktop(outputPath, target, ctx?.signal);
|
|
135
|
+
if (capture.status !== 'passed') {
|
|
136
|
+
if (capture.status === 'aborted') {
|
|
137
|
+
return { status: 'aborted', code: 'ABORTED', retryable: false, output: capture.detail };
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
status: 'error',
|
|
141
|
+
code: capture.status === 'failed' ? 'PROCESS_FAILED' : 'EXECUTION_ERROR',
|
|
142
|
+
retryable: false,
|
|
143
|
+
output: `Unable to capture screenshot: ${capture.detail}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const loaded = await loadImageAttachment(outputPath, {
|
|
147
|
+
maxBytes: MAX_INLINE_BYTES_DEFAULT,
|
|
148
|
+
});
|
|
149
|
+
if (!loaded.ok) {
|
|
150
|
+
return {
|
|
151
|
+
status: 'error',
|
|
152
|
+
code: 'EXECUTION_ERROR',
|
|
153
|
+
retryable: false,
|
|
154
|
+
output: `Screenshot saved to ${requestedPath}, but it could not be attached: ${loaded.reason}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const detail = args.detail === 'low' || args.detail === 'auto' ? args.detail : 'high';
|
|
158
|
+
const { att } = loaded;
|
|
159
|
+
return {
|
|
160
|
+
status: 'success',
|
|
161
|
+
code: 'OK',
|
|
162
|
+
retryable: false,
|
|
163
|
+
output: `Captured ${target} display to ${requestedPath} (${att.bytes} bytes). Visual content is attached to the next model request.`,
|
|
164
|
+
modelAttachments: [{
|
|
165
|
+
type: 'image',
|
|
166
|
+
name: att.name,
|
|
167
|
+
mime: att.mime,
|
|
168
|
+
dataUrl: att.dataUrl,
|
|
169
|
+
detail,
|
|
170
|
+
}],
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { loadImageAttachment, MAX_INLINE_BYTES_DEFAULT, } from '../../attachments/image.js';
|
|
2
|
+
export const viewImageTool = {
|
|
3
|
+
name: 'view_image',
|
|
4
|
+
description: 'View a local image as visual model input. Use this whenever the user refers to a screenshot, UI error, design mockup, Figma export, diagram, or other image file. Supports PNG, JPEG, GIF, and WebP up to 4 MiB.',
|
|
5
|
+
parameters: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
path: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'Image path, relative to the working directory',
|
|
11
|
+
},
|
|
12
|
+
detail: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
enum: ['auto', 'low', 'high'],
|
|
15
|
+
description: 'Vision detail level (default: auto)',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ['path'],
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
},
|
|
21
|
+
async execute(args) {
|
|
22
|
+
const loaded = await loadImageAttachment(String(args.path ?? ''), {
|
|
23
|
+
maxBytes: MAX_INLINE_BYTES_DEFAULT,
|
|
24
|
+
});
|
|
25
|
+
if (!loaded.ok) {
|
|
26
|
+
return {
|
|
27
|
+
status: 'error',
|
|
28
|
+
code: loaded.reason.startsWith('outside sandbox') ? 'SANDBOX_DENIED' : 'EXECUTION_ERROR',
|
|
29
|
+
retryable: false,
|
|
30
|
+
output: `Unable to view image: ${loaded.reason}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const detail = args.detail === 'low' || args.detail === 'high' ? args.detail : 'auto';
|
|
34
|
+
const { att } = loaded;
|
|
35
|
+
return {
|
|
36
|
+
status: 'success',
|
|
37
|
+
code: 'OK',
|
|
38
|
+
retryable: false,
|
|
39
|
+
output: `Viewed image "${att.name}" (${att.mime}, ${att.bytes} bytes). Visual content is attached to the next model request.`,
|
|
40
|
+
modelAttachments: [{
|
|
41
|
+
type: 'image',
|
|
42
|
+
name: att.name,
|
|
43
|
+
mime: att.mime,
|
|
44
|
+
dataUrl: att.dataUrl,
|
|
45
|
+
detail,
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|