@miruzero/zero-agent 0.2.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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Zero Agent
2
+
3
+ Local worker agent for Zero MCP multi-device execution.
4
+
5
+ ## Commands
6
+
7
+ ```powershell
8
+ zero-agent init --server https://zero.miru.work
9
+ zero-agent pair --server https://zero.miru.work --code ZERO-XXXX
10
+ zero-agent start --once
11
+ zero-agent start
12
+ zero-agent status
13
+ zero-agent doctor
14
+ zero-agent providers
15
+ zero-agent tools
16
+ zero-agent capabilities
17
+ ```
18
+
19
+ ## State
20
+
21
+ Default state path:
22
+
23
+ ```text
24
+ %USERPROFILE%\.zero-agent\device.json
25
+ ```
26
+
27
+ Device names default to the local PC hostname. Use --name only when you want a custom display name.
28
+
29
+ Device tokens are local secrets and must not be printed or committed.
30
+
31
+ ## Package checks
32
+
33
+ ```powershell
34
+ npm test
35
+ npm pack --dry-run
36
+ ```
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const cli = require('../src/cli');
5
+
6
+ cli.run(process.argv.slice(2), process.env)
7
+ .then((code) => {
8
+ process.exitCode = code;
9
+ })
10
+ .catch((error) => {
11
+ console.error('[zero-agent] fatal: ' + (error && error.message ? error.message : String(error)));
12
+ process.exitCode = 1;
13
+ });
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@miruzero/zero-agent",
3
+ "version": "0.2.0",
4
+ "description": "MiruZero local worker agent for multi-device tool execution.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "zero-agent": "bin/zero-agent.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test",
11
+ "start": "node bin/zero-agent.js start",
12
+ "doctor": "node bin/zero-agent.js doctor"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "src",
17
+ "README.md"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "license": "UNLICENSED"
23
+ }
package/src/agent.js ADDED
@@ -0,0 +1,111 @@
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
+
8
+ const nowIso = () => new Date().toISOString();
9
+
10
+ const serverBase = (server) => String(server || 'https://zero.miru.work').replace(/\/$/, '');
11
+
12
+ const initDevice = ({ name, server }, env = process.env) => {
13
+ const host = os.hostname();
14
+ const device = {
15
+ name: name || host,
16
+ host,
17
+ server: server || 'https://zero.miru.work',
18
+ device_id: null,
19
+ status: 'initialized',
20
+ created_at: nowIso(),
21
+ updated_at: nowIso()
22
+ };
23
+ const file = config.saveDevice(device, env);
24
+ return { file, device: config.redactedDevice(device) };
25
+ };
26
+
27
+ const pairDevice = async ({ name, server, code }, env = process.env) => {
28
+ if (!server) throw Object.assign(new Error('server is required'), { exitCode: 64 });
29
+ if (!code) throw Object.assign(new Error('pairing code is required'), { exitCode: 64 });
30
+ const host = os.hostname();
31
+ const payload = { name: name || host, pairing_code: code, host };
32
+ const result = await requestJson(`${serverBase(server)}/devices/register`, {
33
+ method: 'POST',
34
+ body: payload
35
+ });
36
+ if (!result.ok) {
37
+ const error = new Error(`pair failed status=${result.status}`);
38
+ error.response = result.json;
39
+ throw error;
40
+ }
41
+ const device = {
42
+ name: payload.name,
43
+ host,
44
+ server,
45
+ device_id: result.json.device_id,
46
+ device_token: result.json.device_token,
47
+ status: 'paired',
48
+ paired_at: nowIso(),
49
+ updated_at: nowIso()
50
+ };
51
+ const file = config.saveDevice(device, env);
52
+ return { file, device: config.redactedDevice(device) };
53
+ };
54
+
55
+ const getPairedDevice = (env = process.env) => {
56
+ const device = config.loadDevice(env);
57
+ if (!device || !device.device_id || !device.device_token || !device.server) {
58
+ return null;
59
+ }
60
+ return device;
61
+ };
62
+
63
+ const heartbeatPayload = (device, env = process.env) => {
64
+ const capability = caps.buildCapabilities(env);
65
+ return {
66
+ name: device.name,
67
+ status: 'online',
68
+ last_seen: nowIso(),
69
+ capability_hash: capability.capability_hash,
70
+ provider_count: capability.providers.length
71
+ };
72
+ };
73
+
74
+ const heartbeatOnce = async (env = process.env) => {
75
+ const device = getPairedDevice(env);
76
+ if (!device) return { ok: false, code: 'NOT_PAIRED', message: 'run zero-agent pair first' };
77
+ const url = `${serverBase(device.server)}/devices/${encodeURIComponent(device.device_id)}/heartbeat`;
78
+ const result = await requestJson(url, {
79
+ method: 'POST',
80
+ token: device.device_token,
81
+ body: heartbeatPayload(device, env)
82
+ });
83
+ return { ok: result.ok, status: result.status, body: result.json };
84
+ };
85
+
86
+ const syncCapabilitiesOnce = async (env = process.env) => {
87
+ const device = getPairedDevice(env);
88
+ if (!device) return { ok: false, code: 'NOT_PAIRED' };
89
+ const url = `${serverBase(device.server)}/devices/${encodeURIComponent(device.device_id)}/capabilities`;
90
+ const result = await requestJson(url, {
91
+ method: 'POST',
92
+ token: device.device_token,
93
+ body: caps.buildCapabilities(env)
94
+ });
95
+ return { ok: result.ok, status: result.status, body: result.json };
96
+ };
97
+
98
+ const listDevices = async (env = process.env) => {
99
+ const device = config.loadDevice(env);
100
+ const result = await requestJson(`${serverBase(device?.server)}/devices`);
101
+ return { ok: result.ok, status: result.status, body: result.json };
102
+ };
103
+ module.exports = {
104
+ initDevice,
105
+ pairDevice,
106
+ heartbeatPayload,
107
+ heartbeatOnce,
108
+ syncCapabilitiesOnce,
109
+ listDevices,
110
+ getPairedDevice
111
+ };
package/src/args.js ADDED
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const parseArgs = (argv = []) => {
4
+ const out = { _: [] };
5
+ for (let index = 0; index < argv.length; index += 1) {
6
+ const item = argv[index];
7
+ if (!item.startsWith('--')) {
8
+ out._.push(item);
9
+ continue;
10
+ }
11
+ const raw = item.slice(2);
12
+ const eq = raw.indexOf('=');
13
+ if (eq >= 0) {
14
+ out[raw.slice(0, eq)] = raw.slice(eq + 1);
15
+ continue;
16
+ }
17
+ const next = argv[index + 1];
18
+ if (next && !next.startsWith('--')) {
19
+ out[raw] = next;
20
+ index += 1;
21
+ } else {
22
+ out[raw] = true;
23
+ }
24
+ }
25
+ return out;
26
+ };
27
+
28
+ module.exports = { parseArgs };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const defaultMcpRoot = () => path.resolve(__dirname, '../..');
8
+ const mcpRoot = (env = process.env) => path.resolve(env.ZERO_MCP_ROOT || defaultMcpRoot());
9
+ const providersDir = (env = process.env) => path.join(mcpRoot(env), 'providers');
10
+
11
+ const readManifest = (file) => {
12
+ try {
13
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
14
+ } catch (error) {
15
+ return { error: error.message };
16
+ }
17
+ };
18
+
19
+ const manifestProviders = (env = process.env) => {
20
+ const dir = providersDir(env);
21
+ if (!fs.existsSync(dir)) return [];
22
+ return fs.readdirSync(dir, { withFileTypes: true })
23
+ .filter((item) => item.isDirectory())
24
+ .map((item) => {
25
+ const root = path.join(dir, item.name);
26
+ const manifestFile = path.join(root, 'provider.json');
27
+ const manifest = fs.existsSync(manifestFile) ? readManifest(manifestFile) : {};
28
+ return {
29
+ name: item.name,
30
+ type: manifest.type || 'unknown',
31
+ description: manifest.description || '',
32
+ providerRoot: root,
33
+ manifestFile,
34
+ manifestError: manifest.error || null,
35
+ module: manifest.module || null,
36
+ command: manifest.command || null,
37
+ args: manifest.args || []
38
+ };
39
+ });
40
+ };
41
+
42
+ const safeInternalTools = (provider) => {
43
+ if (!provider.module) return [];
44
+ const modulePath = provider.module.replaceAll('{providerRoot}', provider.providerRoot.replace(/\\/g, '/'));
45
+ const resolved = path.resolve(modulePath);
46
+ try {
47
+ const runtime = require(resolved);
48
+ if (typeof runtime.listTools !== 'function') return [];
49
+ return runtime.listTools({}).map((tool) => ({
50
+ name: tool.name,
51
+ description: tool.description || '',
52
+ inputSchema: tool.inputSchema || null
53
+ }));
54
+ } catch (error) {
55
+ return [{ name: '__scan_error__', description: error.message, inputSchema: null }];
56
+ }
57
+ };
58
+ const buildCapabilities = (env = process.env) => {
59
+ const providers = manifestProviders(env).map((provider) => {
60
+ const tools = provider.type === 'internal' ? safeInternalTools(provider) : [];
61
+ return {
62
+ name: provider.name,
63
+ type: provider.type,
64
+ description: provider.description,
65
+ status: provider.manifestError ? 'invalid' : 'present',
66
+ tools,
67
+ tool_count: tools.length,
68
+ mcp_stdio_live_scan: provider.type === 'mcp-stdio' ? false : undefined
69
+ };
70
+ });
71
+ const payload = {
72
+ agent_version: require('../package.json').version,
73
+ mcp_root: mcpRoot(env),
74
+ providers
75
+ };
76
+ payload.capability_hash = crypto
77
+ .createHash('sha256')
78
+ .update(JSON.stringify(payload.providers))
79
+ .digest('hex');
80
+ return payload;
81
+ };
82
+
83
+ module.exports = { mcpRoot, providersDir, manifestProviders, buildCapabilities };
package/src/cli.js ADDED
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const childProcess = require('node:child_process');
4
+ const os = require('node:os');
5
+ const pkg = require('../package.json');
6
+ const config = require('./config');
7
+ const caps = require('./capabilities');
8
+ const agent = require('./agent');
9
+ const { parseArgs } = require('./args');
10
+
11
+ const usage = () => `Zero Agent
12
+
13
+ Usage:
14
+ zero-agent setup [--server <url>] [--name <device>] [--code <pairing-code>] [--no-global]
15
+ zero-agent init --server <url> [--name <device>]
16
+ zero-agent pair --server <url> --code <pairing-code> [--name <device>]
17
+ zero-agent start [--once] [--interval <ms>]
18
+ zero-agent status
19
+ zero-agent devices
20
+ zero-agent doctor
21
+ zero-agent providers
22
+ zero-agent tools
23
+ zero-agent capabilities
24
+ `;
25
+
26
+ const printJson = (value) => console.log(JSON.stringify(value, null, 2));
27
+ const okMark = (ok) => ok ? 'OK' : 'FAIL';
28
+ const DEFAULT_SERVER = 'https://zero.miru.work';
29
+
30
+ const runGlobalInstall = (parsed, env) => {
31
+ if (parsed['no-global'] || env.ZERO_AGENT_SKIP_GLOBAL_INSTALL === '1') {
32
+ console.log('globalInstall=SKIPPED');
33
+ return { skipped: true };
34
+ }
35
+ const spec = parsed.package || (pkg.name + '@' + pkg.version);
36
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
37
+ console.log('globalInstall=START ' + spec);
38
+ const result = childProcess.spawnSync(npmCmd, ['install', '-g', spec], { stdio: 'inherit', env });
39
+ if (result.status === 0) {
40
+ console.log('globalInstall=OK');
41
+ return { ok: true };
42
+ }
43
+ console.log('globalInstall=WARN exitCode=' + result.status);
44
+ return { ok: false, exitCode: result.status };
45
+ };
46
+
47
+ const runSetup = async (parsed, env) => {
48
+ const server = parsed.server || DEFAULT_SERVER;
49
+ const name = parsed.name || undefined;
50
+ console.log('Zero Agent Setup');
51
+ console.log('server=' + server);
52
+ console.log('name=' + (name || os.hostname()));
53
+ runGlobalInstall(parsed, env);
54
+ const init = agent.initDevice({ name, server }, env);
55
+ console.log('init=OK file=' + init.file);
56
+ if (!parsed.code) {
57
+ console.log('pairing=NEEDS_CODE');
58
+ console.log('Next: zero-agent pair --server ' + server + ' --code ZERO-XXXXXX --name ' + init.device.name);
59
+ console.log('Or: npx ' + pkg.name + '@latest setup --server ' + server + ' --name ' + init.device.name + ' --code ZERO-XXXXXX');
60
+ return 0;
61
+ }
62
+ const paired = await agent.pairDevice({ name: init.device.name, server, code: parsed.code }, env);
63
+ console.log('pair=OK file=' + paired.file);
64
+ printJson(paired.device);
65
+ const heartbeat = await runStartOnce(env);
66
+ console.log('verify=OK');
67
+ printJson(heartbeat);
68
+ console.log('Start: zero-agent start');
69
+ return 0;
70
+ };
71
+
72
+
73
+ const status = (env) => {
74
+ const device = config.loadDevice(env);
75
+ if (!device) return { paired: false, home: config.agentHome(env) };
76
+ return { paired: Boolean(device.device_id), home: config.agentHome(env), device: config.redactedDevice(device) };
77
+ };
78
+
79
+ const doctor = (env) => {
80
+ const capability = caps.buildCapabilities(env);
81
+ return {
82
+ ok: true,
83
+ node: process.version,
84
+ platform: process.platform,
85
+ agent_home: config.agentHome(env),
86
+ mcp_root: capability.mcp_root,
87
+ providers: capability.providers.length,
88
+ capability_hash: capability.capability_hash
89
+ };
90
+ };
91
+
92
+ const runStartOnce = async (env) => {
93
+ const heartbeat = await agent.heartbeatOnce(env);
94
+ if (!heartbeat.ok && heartbeat.code === 'NOT_PAIRED') return heartbeat;
95
+ const capability = await agent.syncCapabilitiesOnce(env);
96
+ return { heartbeat, capability };
97
+ };
98
+
99
+ const printStartBanner = (env) => {
100
+ const device = config.loadDevice(env);
101
+ console.log('🚀 Starting Zero Agent...');
102
+ console.log('🔁 Session restored: ' + (device?.device_id ? 'yes' : 'no'));
103
+ console.log('👤 Device Name: ' + (device?.name || '(unpaired)'));
104
+ console.log('🆔 Device ID: ' + (device?.device_id || '(not paired)'));
105
+ console.log('🌐 Server: ' + (device?.server || 'https://zero.miru.work'));
106
+ };
107
+
108
+ const printLoopResult = (result) => {
109
+ if (!result || result.code === 'NOT_PAIRED') {
110
+ console.log('❌ Device not paired: run zero-agent pair first');
111
+ return;
112
+ }
113
+ const hb = result.heartbeat || {};
114
+ const cap = result.capability || {};
115
+ const device = cap.body?.device || hb.body?.device || null;
116
+ console.log(JSON.stringify({
117
+ at: new Date().toISOString(),
118
+ heartbeat: okMark(Boolean(hb.ok)),
119
+ capabilities: okMark(Boolean(cap.ok)),
120
+ status: device?.status || null,
121
+ name: device?.name || null,
122
+ host: device?.host || null,
123
+ providers: device?.provider_count || null,
124
+ last_seen: device?.last_seen || null
125
+ }));
126
+ };
127
+
128
+ async function run(argv = [], env = process.env) {
129
+ const parsed = parseArgs(argv);
130
+ const command = parsed._[0] || 'help';
131
+ if (command === 'help' || parsed.help) {
132
+ console.log(usage());
133
+ return 0;
134
+ }
135
+ if (command === 'setup') {
136
+ return runSetup(parsed, env);
137
+ }
138
+ if (command === 'init') {
139
+ const result = agent.initDevice({ name: parsed.name, server: parsed.server }, env);
140
+ console.log(`init=OK file=${result.file}`);
141
+ printJson(result.device);
142
+ return 0;
143
+ }
144
+ if (command === 'pair') {
145
+ const result = await agent.pairDevice({ name: parsed.name, server: parsed.server, code: parsed.code }, env);
146
+ console.log(`pair=OK file=${result.file}`);
147
+ printJson(result.device);
148
+ return 0;
149
+ }
150
+ if (command === 'status') {
151
+ printJson(status(env));
152
+ return 0;
153
+ }
154
+ if (command === 'devices') {
155
+ printJson(await agent.listDevices(env));
156
+ return 0;
157
+ }
158
+ if (command === 'doctor') {
159
+ printJson(doctor(env));
160
+ return 0;
161
+ }
162
+ if (command === 'providers') {
163
+ printJson(caps.manifestProviders(env).map((item) => ({
164
+ name: item.name,
165
+ type: item.type,
166
+ description: item.description,
167
+ status: item.manifestError ? 'invalid' : 'present'
168
+ })));
169
+ return 0;
170
+ }
171
+ if (command === 'tools' || command === 'capabilities') {
172
+ printJson(caps.buildCapabilities(env));
173
+ return 0;
174
+ }
175
+ if (command === 'start') {
176
+ if (parsed.once) {
177
+ printJson(await runStartOnce(env));
178
+ return 0;
179
+ }
180
+ const interval = Number(parsed.interval || 10000);
181
+ printStartBanner(env);
182
+ console.log('⏱️ Heartbeat interval: ' + interval + 'ms');
183
+ console.log('🟢 Device ready; press Ctrl+C to stop');
184
+ while (true) {
185
+ const result = await runStartOnce(env);
186
+ printLoopResult(result);
187
+ await new Promise((resolve) => setTimeout(resolve, interval));
188
+ }
189
+ }
190
+ console.error('unknown command: ' + command);
191
+ console.error(usage());
192
+ return 64;
193
+ }
194
+
195
+ module.exports = { run, usage, status, doctor };
package/src/config.js ADDED
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const defaultHome = () => path.join(os.homedir(), '.zero-agent');
8
+
9
+ const agentHome = (env = process.env) => path.resolve(env.ZERO_AGENT_HOME || defaultHome());
10
+ const deviceFile = (env = process.env) => path.join(agentHome(env), 'device.json');
11
+ const logDir = (env = process.env) => path.join(agentHome(env), 'logs');
12
+
13
+ const ensureDirs = (env = process.env) => {
14
+ fs.mkdirSync(agentHome(env), { recursive: true });
15
+ fs.mkdirSync(logDir(env), { recursive: true });
16
+ };
17
+
18
+ const readJson = (file, fallback = null) => {
19
+ if (!fs.existsSync(file)) return fallback;
20
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ };
22
+ const writeJson = (file, value) => {
23
+ fs.mkdirSync(path.dirname(file), { recursive: true });
24
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', 'utf8');
25
+ };
26
+
27
+ const loadDevice = (env = process.env) => readJson(deviceFile(env), null);
28
+
29
+ const saveDevice = (device, env = process.env) => {
30
+ ensureDirs(env);
31
+ writeJson(deviceFile(env), device);
32
+ return deviceFile(env);
33
+ };
34
+
35
+ const redactedDevice = (device) => {
36
+ if (!device) return null;
37
+ const clone = { ...device };
38
+ if (clone.device_token) clone.device_token = 'REDACTED';
39
+ if (clone.token) clone.token = 'REDACTED';
40
+ return clone;
41
+ };
42
+ module.exports = {
43
+ agentHome,
44
+ deviceFile,
45
+ logDir,
46
+ ensureDirs,
47
+ readJson,
48
+ writeJson,
49
+ loadDevice,
50
+ saveDevice,
51
+ redactedDevice
52
+ };
package/src/http.js ADDED
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const requestJson = async (url, { method = 'GET', token = null, body = null, timeoutMs = 15000 } = {}) => {
4
+ const controller = new AbortController();
5
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
6
+ try {
7
+ const headers = { accept: 'application/json' };
8
+ if (body !== null) headers['content-type'] = 'application/json';
9
+ if (token) headers.authorization = `Bearer ${token}`;
10
+ const response = await fetch(url, {
11
+ method,
12
+ headers,
13
+ body: body === null ? undefined : JSON.stringify(body),
14
+ signal: controller.signal
15
+ });
16
+ const text = await response.text();
17
+ let json = null;
18
+ try { json = text ? JSON.parse(text) : null; } catch { json = { raw: text }; }
19
+ return { ok: response.ok, status: response.status, json };
20
+ } finally {
21
+ clearTimeout(timeout);
22
+ }
23
+ };
24
+
25
+ module.exports = { requestJson };