@miruzero/zero-agent 0.3.3 → 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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/package.json +9 -2
  3. package/src/agent.js +158 -145
  4. package/src/autostart.js +53 -5
  5. package/src/capabilities.js +120 -91
  6. package/src/cli.js +63 -21
  7. package/src/command/filesystem/append-file.js +33 -0
  8. package/src/command/filesystem/copy-directory.js +33 -0
  9. package/src/command/filesystem/copy-file.js +33 -0
  10. package/src/command/filesystem/create-directory.js +30 -0
  11. package/src/command/filesystem/create-file.js +35 -0
  12. package/src/command/filesystem/delete-directory.js +27 -0
  13. package/src/command/filesystem/delete-file.js +33 -0
  14. package/src/command/filesystem/exists.js +29 -0
  15. package/src/command/filesystem/get-file-info.js +39 -0
  16. package/src/command/filesystem/glob-files.js +39 -0
  17. package/src/command/filesystem/grep-files.js +54 -0
  18. package/src/command/filesystem/hash-file.js +35 -0
  19. package/src/command/filesystem/index.js +34 -0
  20. package/src/command/filesystem/list-directory.js +33 -0
  21. package/src/command/filesystem/move-directory.js +30 -0
  22. package/src/command/filesystem/move-file.js +33 -0
  23. package/src/command/filesystem/path-policy.js +31 -0
  24. package/src/command/filesystem/read-file.js +37 -0
  25. package/src/command/filesystem/read-files.js +56 -0
  26. package/src/command/filesystem/rename-file.js +20 -0
  27. package/src/command/filesystem/replace-file.js +45 -0
  28. package/src/command/filesystem/text.js +17 -0
  29. package/src/command/filesystem/touch-file.js +29 -0
  30. package/src/command/filesystem/truncate-file.js +26 -0
  31. package/src/command/filesystem/walk.js +39 -0
  32. package/src/command/filesystem/write-file.js +39 -0
  33. package/src/command/index.js +49 -0
  34. package/src/command/path/index.js +53 -0
  35. package/src/index.js +7 -0
  36. package/src/local-tools.js +205 -156
  37. 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 uses `npx -y @miruzero/zero-agent@<current-version> start` so the user-logon agent does not depend on a stale global install.
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",
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 { requestJson } = require('./http');
7
- const localTools = require('./local-tools');
8
-
9
- const nowIso = () => new Date().toISOString();
10
-
11
- const serverBase = (server) => String(server || 'https://zero.miru.work').replace(/\/$/, '');
12
- const trace = (env, event, payload = {}) => {
13
- if (env.ZERO_TRACE !== '1') return;
14
- console.log(JSON.stringify({ at: nowIso(), event, ...payload }));
15
- };
16
-
17
- const initDevice = ({ name, server }, env = process.env) => {
18
- const host = os.hostname();
19
- const device = {
20
- name: name || host,
21
- host,
22
- server: server || 'https://zero.miru.work',
23
- device_id: null,
24
- status: 'initialized',
25
- created_at: nowIso(),
26
- updated_at: nowIso()
27
- };
28
- const file = config.saveDevice(device, env);
29
- return { file, device: config.redactedDevice(device) };
30
- };
31
-
32
- const pairDevice = async ({ name, server, code }, env = process.env) => {
33
- if (!server) throw Object.assign(new Error('server is required'), { exitCode: 64 });
34
- if (!code) throw Object.assign(new Error('pairing code is required'), { exitCode: 64 });
35
- const host = os.hostname();
36
- const payload = { name: name || host, pairing_code: code, host };
37
- const result = await requestJson(`${serverBase(server)}/devices/register`, {
38
- method: 'POST',
39
- body: payload
40
- });
41
- if (!result.ok) {
42
- const error = new Error(`pair failed status=${result.status}`);
43
- error.response = result.json;
44
- throw error;
45
- }
46
- const device = {
47
- name: payload.name,
48
- host,
49
- server,
50
- device_id: result.json.device_id,
51
- device_token: result.json.device_token,
52
- status: 'paired',
53
- paired_at: nowIso(),
54
- updated_at: nowIso()
55
- };
56
- const file = config.saveDevice(device, env);
57
- return { file, device: config.redactedDevice(device) };
58
- };
59
-
60
- const getPairedDevice = (env = process.env) => {
61
- const device = config.loadDevice(env);
62
- if (!device || !device.device_id || !device.device_token || !device.server) {
63
- return null;
64
- }
65
- return device;
66
- };
67
-
68
- const heartbeatPayload = (device, env = process.env) => {
69
- const capability = caps.buildCapabilities(env);
70
- return {
71
- name: device.name,
72
- status: 'online',
73
- last_seen: nowIso(),
74
- capability_hash: capability.capability_hash,
75
- provider_count: capability.providers.length
76
- };
77
- };
78
-
79
- const heartbeatOnce = async (env = process.env) => {
80
- const device = getPairedDevice(env);
81
- if (!device) return { ok: false, code: 'NOT_PAIRED', message: 'run zero-agent pair first' };
82
- const url = `${serverBase(device.server)}/devices/${encodeURIComponent(device.device_id)}/heartbeat`;
83
- const result = await requestJson(url, {
84
- method: 'POST',
85
- token: device.device_token,
86
- body: heartbeatPayload(device, env)
87
- });
88
- return { ok: result.ok, status: result.status, body: result.json };
89
- };
90
-
91
- const syncCapabilitiesOnce = async (env = process.env) => {
92
- const device = getPairedDevice(env);
93
- if (!device) return { ok: false, code: 'NOT_PAIRED' };
94
- const url = `${serverBase(device.server)}/devices/${encodeURIComponent(device.device_id)}/capabilities`;
95
- const result = await requestJson(url, {
96
- method: 'POST',
97
- token: device.device_token,
98
- body: caps.buildCapabilities(env)
99
- });
100
- return { ok: result.ok, status: result.status, body: result.json };
101
- };
102
-
103
-
104
- const pollTaskOnce = async (env = process.env) => {
105
- const device = getPairedDevice(env);
106
- if (!device) return { ok: false, code: 'NOT_PAIRED' };
107
- const nextUrl = serverBase(device.server) + '/devices/' + encodeURIComponent(device.device_id) + '/tasks/next';
108
- const next = await requestJson(nextUrl, { method: 'GET', token: device.device_token });
109
- if (!next.ok) return { ok: false, status: next.status, body: next.json };
110
- const task = next.json?.task || null;
111
- if (!task) return { ok: true, task: null };
112
- const started = Date.now();
113
- trace(env, 'agent.task.received', { task_id: task.task_id, tool: task.tool, args: task.arguments || {} });
114
- const resultUrl = serverBase(device.server) + '/devices/' + encodeURIComponent(device.device_id)
115
- + '/tasks/' + encodeURIComponent(task.task_id) + '/result';
116
- try {
117
- const result = localTools.execute(task.tool, task.arguments || {}, env);
118
- trace(env, 'agent.task.done', { task_id: task.task_id, ok: true, duration_ms: Date.now() - started });
119
- const posted = await requestJson(resultUrl, { method: 'POST', token: device.device_token, body: { ok: true, result } });
120
- const finalTask = posted.json?.task || task;
121
- return { ok: posted.ok, task: finalTask, result, posted: { status: posted.status, body: posted.json } };
122
- } catch (error) {
123
- const payload = { ok: false, error: { code: error.code || 'AGENT_EXEC_ERROR', message: error.message || String(error) } };
124
- trace(env, 'agent.task.done', { task_id: task.task_id, ok: false, error: payload.error, duration_ms: Date.now() - started });
125
- const posted = await requestJson(resultUrl, { method: 'POST', token: device.device_token, body: payload });
126
- const finalTask = posted.json?.task || task;
127
- return { ok: false, task: finalTask, error: payload.error, posted: { status: posted.status, body: posted.json } };
128
- }
129
- };
130
-
131
- const listDevices = async (env = process.env) => {
132
- const device = config.loadDevice(env);
133
- const result = await requestJson(`${serverBase(device?.server)}/devices`);
134
- return { ok: result.ok, status: result.status, body: result.json };
135
- };
136
- module.exports = {
137
- initDevice,
138
- pairDevice,
139
- heartbeatPayload,
140
- heartbeatOnce,
141
- syncCapabilitiesOnce,
142
- pollTaskOnce,
143
- listDevices,
144
- getPairedDevice
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 = 30000;
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,21 +36,52 @@ const paths = (env = process.env) => {
35
36
  };
36
37
 
37
38
  const packageSpec = () => pkg.name + '@' + pkg.version;
39
+ const localPackageBin = () => path.join(__dirname, '..', 'bin', 'zero-agent.js');
38
40
 
39
- const launcherContent = ({ interval = DEFAULT_INTERVAL, roots, logFile }, env = process.env) => {
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) => {
40
69
  const home = config.agentHome(env);
41
70
  const spec = packageSpec();
71
+ const bin = binFile || packageBin(env);
72
+ const node = nodeFile || nodeBin(env);
42
73
  return [
43
74
  '@echo off',
44
75
  'setlocal',
45
76
  'set "ZERO_TRACE=1"',
46
77
  'set "ZERO_AGENT_HOME=' + winPath(home) + '"',
47
78
  'set "ZERO_AGENT_ROOTS=' + winPath(roots) + '"',
79
+ ...(env.ZERO_MCP_ROOT ? ['set "ZERO_MCP_ROOT=' + winPath(env.ZERO_MCP_ROOT) + '"'] : []),
48
80
  'set "ZERO_AGENT_INTERVAL=' + Number(interval || DEFAULT_INTERVAL) + '"',
49
81
  'rem interval=' + Number(interval || DEFAULT_INTERVAL) + 'ms',
50
82
  'if not exist ' + q(winPath(path.dirname(logFile))) + ' mkdir ' + q(winPath(path.dirname(logFile))),
51
83
  'echo [%date% %time%] starting ' + spec + ' >> ' + q(winPath(logFile)),
52
- 'npx -y ' + spec + ' start --interval %ZERO_AGENT_INTERVAL% >> ' + q(winPath(logFile)) + ' 2>&1',
84
+ q(winPath(node)) + ' ' + q(winPath(bin)) + ' start --interval %ZERO_AGENT_INTERVAL% >> ' + q(winPath(logFile)) + ' 2>&1',
53
85
  ''
54
86
  ].join('\r\n');
55
87
  };
@@ -65,12 +97,25 @@ const prepare = (options = {}, env = process.env) => {
65
97
  const p = paths(env);
66
98
  const interval = Number(options.interval || env.ZERO_AGENT_INTERVAL || DEFAULT_INTERVAL);
67
99
  const roots = options.roots || defaultRoots(env);
100
+ const binFile = packageBin(env);
101
+ const nodeFile = nodeBin(env);
68
102
  fs.mkdirSync(path.dirname(p.launcherFile), { recursive: true });
69
103
  fs.mkdirSync(path.dirname(p.startupFile), { recursive: true });
70
104
  fs.mkdirSync(path.dirname(p.logFile), { recursive: true });
71
- fs.writeFileSync(p.launcherFile, launcherContent({ interval, roots, logFile: p.logFile }, env), 'utf8');
105
+ fs.writeFileSync(p.launcherFile, launcherContent({
106
+ interval,
107
+ roots,
108
+ logFile: p.logFile,
109
+ binFile,
110
+ nodeFile
111
+ }, env), 'utf8');
72
112
  fs.writeFileSync(p.startupFile, vbsContent(p.launcherFile), 'utf8');
73
- 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);
74
119
  };
75
120
 
76
121
  const autostart = {
@@ -78,6 +123,9 @@ const autostart = {
78
123
  defaultRoots,
79
124
  paths,
80
125
  prepare,
126
+ isPrepared,
127
+ packageBin,
128
+ nodeBin,
81
129
  launcherContent
82
130
  };
83
131