@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 +36 -0
- package/bin/zero-agent.js +13 -0
- package/package.json +23 -0
- package/src/agent.js +111 -0
- package/src/args.js +28 -0
- package/src/capabilities.js +83 -0
- package/src/cli.js +195 -0
- package/src/config.js +52 -0
- package/src/http.js +25 -0
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 };
|