@miruzero/zero-agent 0.3.4 → 0.3.5
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 +1 -1
- package/package.json +9 -2
- package/src/agent.js +158 -145
- package/src/autostart.js +53 -7
- package/src/capabilities.js +120 -91
- package/src/cli.js +63 -21
- package/src/command/filesystem/append-file.js +33 -0
- package/src/command/filesystem/copy-directory.js +33 -0
- package/src/command/filesystem/copy-file.js +33 -0
- package/src/command/filesystem/create-directory.js +30 -0
- package/src/command/filesystem/create-file.js +35 -0
- package/src/command/filesystem/delete-directory.js +27 -0
- package/src/command/filesystem/delete-file.js +33 -0
- package/src/command/filesystem/exists.js +29 -0
- package/src/command/filesystem/get-file-info.js +39 -0
- package/src/command/filesystem/glob-files.js +39 -0
- package/src/command/filesystem/grep-files.js +54 -0
- package/src/command/filesystem/hash-file.js +35 -0
- package/src/command/filesystem/index.js +34 -0
- package/src/command/filesystem/list-directory.js +33 -0
- package/src/command/filesystem/move-directory.js +30 -0
- package/src/command/filesystem/move-file.js +33 -0
- package/src/command/filesystem/path-policy.js +31 -0
- package/src/command/filesystem/read-file.js +37 -0
- package/src/command/filesystem/read-files.js +56 -0
- package/src/command/filesystem/rename-file.js +20 -0
- package/src/command/filesystem/replace-file.js +45 -0
- package/src/command/filesystem/text.js +17 -0
- package/src/command/filesystem/touch-file.js +29 -0
- package/src/command/filesystem/truncate-file.js +26 -0
- package/src/command/filesystem/walk.js +39 -0
- package/src/command/filesystem/write-file.js +39 -0
- package/src/command/index.js +49 -0
- package/src/command/path/index.js +53 -0
- package/src/index.js +7 -0
- package/src/local-tools.js +205 -171
- package/src/zero-runtime.js +13 -0
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Default state path:
|
|
|
33
33
|
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\MiruZeroAgent.vbs
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
The launcher
|
|
36
|
+
`setup` installs a stable global Zero Agent package by default. The generated launcher records the resolved Node executable and installed Zero Agent entrypoint, while the long-running agent retries transient connection failures instead of exiting.
|
|
37
37
|
|
|
38
38
|
Device tokens are local secrets and must not be printed or committed.
|
|
39
39
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miruzero/zero-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "MiruZero local worker agent for multi-device tool execution.",
|
|
5
5
|
"type": "commonjs",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
6
10
|
"bin": {
|
|
7
11
|
"zero-agent": "bin/zero-agent.js"
|
|
8
12
|
},
|
|
@@ -19,5 +23,8 @@
|
|
|
19
23
|
"engines": {
|
|
20
24
|
"node": ">=20"
|
|
21
25
|
},
|
|
22
|
-
"license": "UNLICENSED"
|
|
26
|
+
"license": "UNLICENSED",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@miruzero/zero": "0.1.0"
|
|
29
|
+
}
|
|
23
30
|
}
|
package/src/agent.js
CHANGED
|
@@ -1,145 +1,158 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
const config = require('./config');
|
|
5
|
-
const caps = require('./capabilities');
|
|
6
|
-
const
|
|
7
|
-
const localTools = require('./local-tools');
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const caps = require('./capabilities');
|
|
6
|
+
const http = require('./http');
|
|
7
|
+
const localTools = require('./local-tools');
|
|
8
|
+
const command = require('./command');
|
|
9
|
+
const zero = require('./zero-runtime');
|
|
10
|
+
|
|
11
|
+
const nowIso = () => new Date().toISOString();
|
|
12
|
+
|
|
13
|
+
const serverBase = (server) => String(server || 'https://zero.miru.work').replace(/\/$/, '');
|
|
14
|
+
const trace = (env, event, payload = {}) => {
|
|
15
|
+
if (env.ZERO_TRACE !== '1') return;
|
|
16
|
+
console.log(JSON.stringify({ at: nowIso(), event, ...payload }));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const initDevice = ({ name, server }, env = process.env) => {
|
|
20
|
+
const host = os.hostname();
|
|
21
|
+
const device = {
|
|
22
|
+
name: name || host,
|
|
23
|
+
host,
|
|
24
|
+
server: server || 'https://zero.miru.work',
|
|
25
|
+
device_id: null,
|
|
26
|
+
status: 'initialized',
|
|
27
|
+
created_at: nowIso(),
|
|
28
|
+
updated_at: nowIso()
|
|
29
|
+
};
|
|
30
|
+
const file = config.saveDevice(device, env);
|
|
31
|
+
return { file, device: config.redactedDevice(device) };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const pairDevice = async ({ name, server, code }, env = process.env) => {
|
|
35
|
+
if (!server) throw Object.assign(new Error('server is required'), { exitCode: 64 });
|
|
36
|
+
if (!code) throw Object.assign(new Error('pairing code is required'), { exitCode: 64 });
|
|
37
|
+
const host = os.hostname();
|
|
38
|
+
const payload = { name: name || host, pairing_code: code, host };
|
|
39
|
+
const result = await http.requestJson(`${serverBase(server)}/devices/register`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
body: payload
|
|
42
|
+
});
|
|
43
|
+
if (!result.ok) {
|
|
44
|
+
const error = new Error(`pair failed status=${result.status}`);
|
|
45
|
+
error.response = result.json;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
const device = {
|
|
49
|
+
name: payload.name,
|
|
50
|
+
host,
|
|
51
|
+
server,
|
|
52
|
+
device_id: result.json.device_id,
|
|
53
|
+
device_token: result.json.device_token,
|
|
54
|
+
status: 'paired',
|
|
55
|
+
paired_at: nowIso(),
|
|
56
|
+
updated_at: nowIso()
|
|
57
|
+
};
|
|
58
|
+
const file = config.saveDevice(device, env);
|
|
59
|
+
return { file, device: config.redactedDevice(device) };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getPairedDevice = (env = process.env) => {
|
|
63
|
+
const device = config.loadDevice(env);
|
|
64
|
+
if (!device || !device.device_id || !device.device_token || !device.server) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return device;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const heartbeatPayload = (device, env = process.env) => {
|
|
71
|
+
const capability = caps.buildCapabilities(env);
|
|
72
|
+
return {
|
|
73
|
+
name: device.name,
|
|
74
|
+
status: 'online',
|
|
75
|
+
last_seen: nowIso(),
|
|
76
|
+
capability_hash: capability.capability_hash,
|
|
77
|
+
provider_count: capability.providers.length
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const heartbeatOnce = async (env = process.env) => {
|
|
82
|
+
const device = getPairedDevice(env);
|
|
83
|
+
if (!device) return { ok: false, code: 'NOT_PAIRED', message: 'run zero-agent pair first' };
|
|
84
|
+
const url = `${serverBase(device.server)}/devices/${encodeURIComponent(device.device_id)}/heartbeat`;
|
|
85
|
+
const result = await http.requestJson(url, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
token: device.device_token,
|
|
88
|
+
body: heartbeatPayload(device, env)
|
|
89
|
+
});
|
|
90
|
+
return { ok: result.ok, status: result.status, body: result.json };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const syncCapabilitiesOnce = async (env = process.env) => {
|
|
94
|
+
const device = getPairedDevice(env);
|
|
95
|
+
if (!device) return { ok: false, code: 'NOT_PAIRED' };
|
|
96
|
+
const url = `${serverBase(device.server)}/devices/${encodeURIComponent(device.device_id)}/capabilities`;
|
|
97
|
+
const result = await http.requestJson(url, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
token: device.device_token,
|
|
100
|
+
body: caps.buildCapabilities(env)
|
|
101
|
+
});
|
|
102
|
+
return { ok: result.ok, status: result.status, body: result.json };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
const executeTask = (task = {}, env = process.env) => {
|
|
107
|
+
if (task.contractVersion) zero.contract.assertCompatible(task.contractVersion);
|
|
108
|
+
if (task.deadline && Date.parse(task.deadline) <= Date.now()) {
|
|
109
|
+
throw Object.assign(new Error('task deadline exceeded'), { code: 'TASK_DEADLINE_EXCEEDED', retryable: false });
|
|
110
|
+
}
|
|
111
|
+
const tool = String(task.tool || '');
|
|
112
|
+
if (tool.startsWith('zero.command.') || tool === 'readFile' || tool === 'command.readFile') {
|
|
113
|
+
try { return command.callTool(tool, task.arguments || {}, { env }); }
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code !== 'UNKNOWN_TOOL') throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return localTools.execute(tool, task.arguments || {}, env);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const pollTaskOnce = async (env = process.env) => {
|
|
122
|
+
const device = getPairedDevice(env);
|
|
123
|
+
if (!device) return { ok: false, code: 'NOT_PAIRED' };
|
|
124
|
+
const nextUrl = serverBase(device.server) + '/devices/' + encodeURIComponent(device.device_id) + '/tasks/next';
|
|
125
|
+
const next = await http.requestJson(nextUrl, { method: 'GET', token: device.device_token });
|
|
126
|
+
if (!next.ok) return { ok: false, status: next.status, body: next.json };
|
|
127
|
+
const task = next.json?.task || null;
|
|
128
|
+
if (!task) return { ok: true, task: null };
|
|
129
|
+
const started = Date.now();
|
|
130
|
+
trace(env, 'agent.task.received', { task_id: task.task_id, tool: task.tool, args: task.arguments || {} });
|
|
131
|
+
const resultUrl = serverBase(device.server) + '/devices/' + encodeURIComponent(device.device_id)
|
|
132
|
+
+ '/tasks/' + encodeURIComponent(task.task_id) + '/result';
|
|
133
|
+
try {
|
|
134
|
+
const result = executeTask(task, env);
|
|
135
|
+
trace(env, 'agent.task.done', { task_id: task.task_id, ok: true, duration_ms: Date.now() - started });
|
|
136
|
+
const posted = await http.requestJson(resultUrl, { method: 'POST', token: device.device_token, body: { ok: true, result } });
|
|
137
|
+
const finalTask = posted.json?.task || task;
|
|
138
|
+
return { ok: posted.ok, task: finalTask, result, posted: { status: posted.status, body: posted.json } };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const payload = { ok: false, error: { code: error.code || 'AGENT_EXEC_ERROR', message: error.message || String(error) } };
|
|
141
|
+
trace(env, 'agent.task.done', { task_id: task.task_id, ok: false, error: payload.error, duration_ms: Date.now() - started });
|
|
142
|
+
const posted = await http.requestJson(resultUrl, { method: 'POST', token: device.device_token, body: payload });
|
|
143
|
+
const finalTask = posted.json?.task || task;
|
|
144
|
+
return { ok: false, task: finalTask, error: payload.error, posted: { status: posted.status, body: posted.json } };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const listDevices = async (env = process.env) => {
|
|
149
|
+
const device = config.loadDevice(env);
|
|
150
|
+
const result = await http.requestJson(`${serverBase(device?.server)}/devices`);
|
|
151
|
+
return { ok: result.ok, status: result.status, body: result.json };
|
|
152
|
+
};
|
|
153
|
+
const agent = {
|
|
154
|
+
initDevice, pairDevice, heartbeatPayload, heartbeatOnce, syncCapabilitiesOnce,
|
|
155
|
+
executeTask, pollTaskOnce, listDevices, getPairedDevice
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
Object.assign(exports, agent);
|
package/src/autostart.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const childProcess = require('node:child_process');
|
|
3
4
|
const fs = require('node:fs');
|
|
4
5
|
const os = require('node:os');
|
|
5
6
|
const path = require('node:path');
|
|
6
7
|
const config = require('./config');
|
|
7
8
|
const pkg = require('../package.json');
|
|
8
9
|
|
|
9
|
-
const DEFAULT_INTERVAL =
|
|
10
|
+
const DEFAULT_INTERVAL = 5000;
|
|
10
11
|
|
|
11
12
|
const q = (value) => '"' + String(value).replace(/"/g, '""') + '"';
|
|
12
13
|
const winPath = (value) => String(value).replace(/\//g, '\\');
|
|
@@ -35,23 +36,52 @@ const paths = (env = process.env) => {
|
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
const packageSpec = () => pkg.name + '@' + pkg.version;
|
|
38
|
-
const
|
|
39
|
+
const localPackageBin = () => path.join(__dirname, '..', 'bin', 'zero-agent.js');
|
|
39
40
|
|
|
40
|
-
const
|
|
41
|
+
const globalRoot = (env = process.env) => {
|
|
42
|
+
if (env.ZERO_AGENT_GLOBAL_ROOT) return path.resolve(env.ZERO_AGENT_GLOBAL_ROOT);
|
|
43
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
44
|
+
const result = childProcess.spawnSync(npmCmd, ['root', '-g'], {
|
|
45
|
+
env,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
windowsHide: true
|
|
48
|
+
});
|
|
49
|
+
if (result.status !== 0) return null;
|
|
50
|
+
const value = String(result.stdout || '').trim();
|
|
51
|
+
return value ? path.resolve(value) : null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const packageBin = (env = process.env) => {
|
|
55
|
+
if (env.ZERO_AGENT_BIN) return path.resolve(env.ZERO_AGENT_BIN);
|
|
56
|
+
if (env.ZERO_AGENT_SKIP_GLOBAL_INSTALL !== '1') {
|
|
57
|
+
const root = globalRoot(env);
|
|
58
|
+
const candidate = root
|
|
59
|
+
? path.join(root, '@miruzero', 'zero-agent', 'bin', 'zero-agent.js')
|
|
60
|
+
: null;
|
|
61
|
+
if (candidate && fs.existsSync(candidate)) return candidate;
|
|
62
|
+
}
|
|
63
|
+
return localPackageBin();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const nodeBin = (env = process.env) => path.resolve(env.ZERO_AGENT_NODE || process.execPath);
|
|
67
|
+
|
|
68
|
+
const launcherContent = ({ interval = DEFAULT_INTERVAL, roots, logFile, binFile, nodeFile }, env = process.env) => {
|
|
41
69
|
const home = config.agentHome(env);
|
|
42
70
|
const spec = packageSpec();
|
|
43
|
-
const bin = packageBin();
|
|
71
|
+
const bin = binFile || packageBin(env);
|
|
72
|
+
const node = nodeFile || nodeBin(env);
|
|
44
73
|
return [
|
|
45
74
|
'@echo off',
|
|
46
75
|
'setlocal',
|
|
47
76
|
'set "ZERO_TRACE=1"',
|
|
48
77
|
'set "ZERO_AGENT_HOME=' + winPath(home) + '"',
|
|
49
78
|
'set "ZERO_AGENT_ROOTS=' + winPath(roots) + '"',
|
|
79
|
+
...(env.ZERO_MCP_ROOT ? ['set "ZERO_MCP_ROOT=' + winPath(env.ZERO_MCP_ROOT) + '"'] : []),
|
|
50
80
|
'set "ZERO_AGENT_INTERVAL=' + Number(interval || DEFAULT_INTERVAL) + '"',
|
|
51
81
|
'rem interval=' + Number(interval || DEFAULT_INTERVAL) + 'ms',
|
|
52
82
|
'if not exist ' + q(winPath(path.dirname(logFile))) + ' mkdir ' + q(winPath(path.dirname(logFile))),
|
|
53
83
|
'echo [%date% %time%] starting ' + spec + ' >> ' + q(winPath(logFile)),
|
|
54
|
-
|
|
84
|
+
q(winPath(node)) + ' ' + q(winPath(bin)) + ' start --interval %ZERO_AGENT_INTERVAL% >> ' + q(winPath(logFile)) + ' 2>&1',
|
|
55
85
|
''
|
|
56
86
|
].join('\r\n');
|
|
57
87
|
};
|
|
@@ -67,12 +97,25 @@ const prepare = (options = {}, env = process.env) => {
|
|
|
67
97
|
const p = paths(env);
|
|
68
98
|
const interval = Number(options.interval || env.ZERO_AGENT_INTERVAL || DEFAULT_INTERVAL);
|
|
69
99
|
const roots = options.roots || defaultRoots(env);
|
|
100
|
+
const binFile = packageBin(env);
|
|
101
|
+
const nodeFile = nodeBin(env);
|
|
70
102
|
fs.mkdirSync(path.dirname(p.launcherFile), { recursive: true });
|
|
71
103
|
fs.mkdirSync(path.dirname(p.startupFile), { recursive: true });
|
|
72
104
|
fs.mkdirSync(path.dirname(p.logFile), { recursive: true });
|
|
73
|
-
fs.writeFileSync(p.launcherFile, launcherContent({
|
|
105
|
+
fs.writeFileSync(p.launcherFile, launcherContent({
|
|
106
|
+
interval,
|
|
107
|
+
roots,
|
|
108
|
+
logFile: p.logFile,
|
|
109
|
+
binFile,
|
|
110
|
+
nodeFile
|
|
111
|
+
}, env), 'utf8');
|
|
74
112
|
fs.writeFileSync(p.startupFile, vbsContent(p.launcherFile), 'utf8');
|
|
75
|
-
return { ok: true, interval, roots, ...p };
|
|
113
|
+
return { ok: true, interval, roots, binFile, nodeFile, ...p };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const isPrepared = (env = process.env) => {
|
|
117
|
+
const p = paths(env);
|
|
118
|
+
return fs.existsSync(p.launcherFile) && fs.existsSync(p.startupFile);
|
|
76
119
|
};
|
|
77
120
|
|
|
78
121
|
const autostart = {
|
|
@@ -80,6 +123,9 @@ const autostart = {
|
|
|
80
123
|
defaultRoots,
|
|
81
124
|
paths,
|
|
82
125
|
prepare,
|
|
126
|
+
isPrepared,
|
|
127
|
+
packageBin,
|
|
128
|
+
nodeBin,
|
|
83
129
|
launcherContent
|
|
84
130
|
};
|
|
85
131
|
|