@miruzero/zero-agent 0.3.6 → 0.3.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 +31 -22
- package/package.json +6 -3
- package/src/agent.js +193 -167
- package/src/args.js +1 -3
- package/src/autostart.js +44 -68
- package/src/capabilities.js +95 -138
- package/src/chatgpt/auth/index.js +114 -0
- package/src/chatgpt/config/paths.js +25 -0
- package/src/chatgpt/index.js +5 -0
- package/src/chatgpt/session/direct-transport.js +68 -0
- package/src/chatgpt/session/exchange.js +17 -0
- package/src/chatgpt/session/index.js +102 -0
- package/src/chatgpt/session/local-storage.js +106 -0
- package/src/chatgpt/transport/direct/http-client.js +191 -0
- package/src/chatgpt-auth.js +17 -111
- package/src/chatgpt-runtime.js +8 -0
- package/src/chatgpt-session-exchange.js +8 -0
- package/src/chatgpt-session.js +26 -0
- package/src/cli.js +108 -71
- package/src/command/exec.js +34 -0
- package/src/command/filesystem/append-file.js +7 -12
- package/src/command/filesystem/copy-directory.js +8 -12
- package/src/command/filesystem/copy-file.js +8 -13
- package/src/command/filesystem/create-directory.js +7 -11
- package/src/command/filesystem/create-file.js +8 -13
- package/src/command/filesystem/delete-directory.js +7 -11
- package/src/command/filesystem/delete-file.js +8 -12
- package/src/command/filesystem/exists.js +6 -10
- package/src/command/filesystem/get-file-info.js +7 -12
- package/src/command/filesystem/glob-files.js +8 -13
- package/src/command/filesystem/grep-files.js +9 -15
- package/src/command/filesystem/hash-file.js +6 -11
- package/src/command/filesystem/index.js +33 -34
- package/src/command/filesystem/list-directory.js +6 -10
- package/src/command/filesystem/move-directory.js +9 -14
- package/src/command/filesystem/move-file.js +8 -13
- package/src/command/filesystem/path-policy.js +33 -16
- package/src/command/filesystem/read-file.js +7 -11
- package/src/command/filesystem/read-files.js +5 -8
- package/src/command/filesystem/rename-file.js +5 -8
- package/src/command/filesystem/replace-file.js +7 -12
- package/src/command/filesystem/text.js +5 -7
- package/src/command/filesystem/touch-file.js +8 -13
- package/src/command/filesystem/truncate-file.js +6 -10
- package/src/command/filesystem/walk.js +36 -39
- package/src/command/filesystem/write-file.js +8 -13
- package/src/command/index.js +19 -24
- package/src/command/path/index.js +46 -53
- package/src/config.js +21 -33
- package/src/device-paths.js +53 -0
- package/src/enrollment-client.js +19 -0
- package/src/http.js +27 -2
- package/src/identity-state.js +27 -0
- package/src/index.js +2 -4
- package/src/instance.js +85 -0
- package/src/local-server.js +174 -0
- package/src/local-tools.js +205 -205
- package/src/shared-zero/catalog.js +33 -0
- package/src/shared-zero/contract.js +18 -0
- package/src/shared-zero/descriptor.js +25 -0
- package/src/shared-zero/errors.js +12 -0
- package/src/shared-zero/index.js +9 -0
- package/src/shared-zero/runtime.js +14 -0
- package/src/shared-zero/target.js +19 -0
- package/src/shared-zero/tool-id.js +26 -0
- package/src/storage-roots.js +21 -0
- package/src/zero-runtime.js +1 -11
- package/src/zero.js +78 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('./zero');
|
|
4
|
+
|
|
5
|
+
const json = (res, status, body) => {
|
|
6
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
7
|
+
res.end(JSON.stringify(body));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const htmlEscape = (value) => String(value)
|
|
11
|
+
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
|
12
|
+
.replaceAll('"', '"').replaceAll("'", ''');
|
|
13
|
+
|
|
14
|
+
const html = (res, status, body) => {
|
|
15
|
+
res.writeHead(status, { 'content-type': 'text/html; charset=utf-8' });
|
|
16
|
+
res.end(String(body));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const preflightHeaders = {
|
|
20
|
+
'content-type': 'application/json',
|
|
21
|
+
'access-control-allow-origin': '*',
|
|
22
|
+
'access-control-allow-methods': 'GET, OPTIONS',
|
|
23
|
+
'access-control-allow-private-network': 'true',
|
|
24
|
+
'cache-control': 'no-store'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const preflightJson = (res, status, body) => {
|
|
28
|
+
res.writeHead(status, preflightHeaders);
|
|
29
|
+
res.end(JSON.stringify(body));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const loadSessionProvider = (env) => {
|
|
33
|
+
if (!env.ZERO_CHATGPT_SESSION_MODULE) return zero.chatgptSession;
|
|
34
|
+
const resolved = require.resolve(env.ZERO_CHATGPT_SESSION_MODULE);
|
|
35
|
+
delete require.cache[resolved];
|
|
36
|
+
return require(resolved);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const reportCompletion = async ({ device, ticket, authStatus, errorCode = '' }) => {
|
|
40
|
+
const coreBase = String(device.server).replace(/\/$/, '');
|
|
41
|
+
const result = await zero.http.requestJson(coreBase + '/setup/chatgpt-auth/complete', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body: { ticket, device: device.name || '', auth_status: authStatus, error_code: errorCode },
|
|
44
|
+
timeoutMs: 3000
|
|
45
|
+
});
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
const error = new Error('Zero setup completion failed');
|
|
48
|
+
error.code = 'ZERO_SETUP_COMPLETE_FAILED';
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const reportReadiness = async ({ device, ticket, state, checks = {}, errorCode = '', httpStatus = null }) => {
|
|
55
|
+
const coreBase = String(device.server).replace(/\/$/, '');
|
|
56
|
+
const result = await zero.http.requestJson(coreBase + '/setup/chatgpt-auth/report', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body: { ticket, device: device.name || '', state, checks, error_code: errorCode, http_status: httpStatus },
|
|
59
|
+
timeoutMs: 3000
|
|
60
|
+
});
|
|
61
|
+
if (!result.ok) {
|
|
62
|
+
const error = new Error('Zero readiness report failed');
|
|
63
|
+
error.code = 'ZERO_READINESS_REPORT_FAILED';
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const reportFailure = async ({ device, ticket, code }) => {
|
|
70
|
+
try { await reportCompletion({ device, ticket, authStatus: 'failed', errorCode: code }); } catch {}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
exports.startLocalServer = ({ port, env = process.env } = {}) => {
|
|
74
|
+
const listenPort = port ?? Number(env.ZERO_AGENT_LOCAL_PORT || 8051);
|
|
75
|
+
const server = zero.nodeHttp.createServer(async (req, res) => {
|
|
76
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
77
|
+
if (req.method === 'GET' && url.pathname === '/zero/status') {
|
|
78
|
+
return json(res, 200, { ok: true, agent: 'zero-agent', device: zero.agent.getPairedDevice(env)?.name || null });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (req.method === 'OPTIONS' && url.pathname === '/zero/preflight') {
|
|
82
|
+
res.writeHead(204, preflightHeaders);
|
|
83
|
+
return res.end();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (req.method === 'GET' && url.pathname === '/zero/preflight') {
|
|
87
|
+
const ticket = url.searchParams.get('ticket') || '';
|
|
88
|
+
const device = zero.agent.getPairedDevice(env);
|
|
89
|
+
if (!ticket) return preflightJson(res, 400, { ok: false, state: 'ticket_required', reported: false });
|
|
90
|
+
if (!device?.server) return preflightJson(res, 503, { ok: false, state: 'agent_unpaired', reported: false });
|
|
91
|
+
let auth;
|
|
92
|
+
try {
|
|
93
|
+
auth = zero.chatgptAuth.status({ env });
|
|
94
|
+
} catch {
|
|
95
|
+
let reported = false;
|
|
96
|
+
try {
|
|
97
|
+
await reportReadiness({
|
|
98
|
+
device, ticket, state: 'runtime_unavailable', errorCode: 'CHATGPT_RUNTIME_UNAVAILABLE',
|
|
99
|
+
checks: { local: 'ready', agent: 'ready', runtime: 'unavailable', auth: 'unknown' }
|
|
100
|
+
});
|
|
101
|
+
reported = true;
|
|
102
|
+
} catch {}
|
|
103
|
+
return preflightJson(res, 503, { ok: false, state: 'runtime_unavailable', reported });
|
|
104
|
+
}
|
|
105
|
+
const valid = Boolean(auth?.auth?.present && auth.auth.status === 'valid');
|
|
106
|
+
const state = valid ? 'auth_valid' : 'auth_required';
|
|
107
|
+
try {
|
|
108
|
+
await reportReadiness({
|
|
109
|
+
device, ticket, state,
|
|
110
|
+
checks: { local: 'ready', agent: 'ready', runtime: 'ready', auth: valid ? 'valid' : 'required' }
|
|
111
|
+
});
|
|
112
|
+
} catch {
|
|
113
|
+
return preflightJson(res, 502, { ok: false, state: 'core_report_failed', reported: false });
|
|
114
|
+
}
|
|
115
|
+
return preflightJson(res, 200, { ok: true, state: 'reported' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (req.method === 'GET' && url.pathname === '/zero/login') {
|
|
119
|
+
const ticket = url.searchParams.get('ticket') || '';
|
|
120
|
+
const device = zero.agent.getPairedDevice(env);
|
|
121
|
+
if (!ticket) return html(res, 400, '<h1>Missing Zero setup ticket</h1>');
|
|
122
|
+
if (!device?.server) return html(res, 503, '<h1>Zero Agent is not paired</h1>');
|
|
123
|
+
|
|
124
|
+
let existingAuth;
|
|
125
|
+
try { existingAuth = zero.chatgptAuth.status({ env }); } catch {
|
|
126
|
+
await reportFailure({ device, ticket, code: 'CHATGPT_RUNTIME_UNAVAILABLE' });
|
|
127
|
+
return html(res, 502, '<h1>ChatGPT runtime unavailable</h1>');
|
|
128
|
+
}
|
|
129
|
+
if (!existingAuth.auth.present || existingAuth.auth.status !== 'valid') {
|
|
130
|
+
try {
|
|
131
|
+
await reportReadiness({
|
|
132
|
+
device, ticket, state: 'auth_checking',
|
|
133
|
+
checks: { local: 'ready', agent: 'ready', runtime: 'ready', auth: 'checking' }
|
|
134
|
+
});
|
|
135
|
+
const sessionResult = await loadSessionProvider(env).fetchSession({ env });
|
|
136
|
+
zero.chatgptAuth.importAuth({
|
|
137
|
+
input: JSON.stringify(sessionResult.body),
|
|
138
|
+
env,
|
|
139
|
+
source: 'zero-agent-session-api'
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const rawCode = String(error.code || 'CHATGPT_SESSION_FETCH_FAILED');
|
|
143
|
+
await reportFailure({ device, ticket, code: rawCode });
|
|
144
|
+
return html(res, 502, `<h1>ChatGPT session fetch failed</h1><p>${htmlEscape(rawCode)}</p>`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const auth = zero.chatgptAuth.status({ env });
|
|
149
|
+
try {
|
|
150
|
+
await reportCompletion({ device, ticket, authStatus: auth.auth.status || 'unknown' });
|
|
151
|
+
} catch {
|
|
152
|
+
return html(res, 502, '<!doctype html><html><head><meta charset="utf-8"><title>Zero Login Failed</title></head><body><h1>Zero login failed</h1><p>ZERO_SETUP_COMPLETE_FAILED</p></body></html>');
|
|
153
|
+
}
|
|
154
|
+
return html(res, 200, `<!doctype html><html><head><meta charset="utf-8"><title>Zero Connected</title></head><body><h1>Zero connected</h1><p>Device: ${htmlEscape(device.name || 'unknown')}</p><p>ChatGPT auth: ${htmlEscape(auth.auth.status || 'unknown')}</p><script>history.replaceState(null,'','/zero/login');setTimeout(()=>window.close(),450);</script></body></html>`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (req.method !== 'POST' || url.pathname !== '/zero/action') {
|
|
158
|
+
return json(res, 404, { ok: false, error: 'NOT_FOUND' });
|
|
159
|
+
}
|
|
160
|
+
let body = '';
|
|
161
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
162
|
+
req.on('end', () => {
|
|
163
|
+
try {
|
|
164
|
+
const task = JSON.parse(body || '{}');
|
|
165
|
+
json(res, 200, { ok: true, result: zero.agent.executeTask(task, env) });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
json(res, 400, { ok: false, error: { code: error.code || 'LOCAL_ACTION_ERROR', message: error.message || String(error) } });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
server.listen(listenPort, '127.0.0.1', () => resolve({ server, port: server.address().port }));
|
|
173
|
+
});
|
|
174
|
+
};
|
package/src/local-tools.js
CHANGED
|
@@ -1,205 +1,205 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
fs.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
{ name: '
|
|
185
|
-
{ name: '
|
|
186
|
-
{ name: '
|
|
187
|
-
{ name: '
|
|
188
|
-
{ name: '
|
|
189
|
-
{ name: '
|
|
190
|
-
{ name: '
|
|
191
|
-
{ name: '
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('./zero');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const textDecoder = new TextDecoder('utf-8', { fatal: false });
|
|
7
|
+
const err = (code, message) => Object.assign(new Error(message), { code });
|
|
8
|
+
const normalize = (value) => zero.path.resolve(String(value)).toLowerCase();
|
|
9
|
+
|
|
10
|
+
const rootsFor = (env = process.env) => {
|
|
11
|
+
const configured = String(env.ZERO_AGENT_ROOTS || '')
|
|
12
|
+
.split(';')
|
|
13
|
+
.map((item) => item.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
const roots = configured.length ? configured : [zero.os.homedir(), process.cwd()];
|
|
16
|
+
return roots.map((item) => zero.path.resolve(item));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const resolveAllowed = (inputPath, env = process.env) => {
|
|
20
|
+
if (!inputPath || typeof inputPath !== 'string') throw err('INVALID_PATH', 'path is required');
|
|
21
|
+
const roots = rootsFor(env);
|
|
22
|
+
const target = zero.path.resolve(inputPath);
|
|
23
|
+
const targetNorm = normalize(target);
|
|
24
|
+
const ok = roots.some((root) => {
|
|
25
|
+
const rootNorm = normalize(root);
|
|
26
|
+
return targetNorm === rootNorm || targetNorm.startsWith(rootNorm + zero.path.sep.toLowerCase());
|
|
27
|
+
});
|
|
28
|
+
if (!ok) throw err('PATH_NOT_ALLOWED', 'path is outside agent allowed roots');
|
|
29
|
+
return target;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const readUtf8 = (file) => {
|
|
33
|
+
const data = zero.fs.readFileSync(file);
|
|
34
|
+
if (data.includes(0)) throw err('BINARY_FILE', 'binary file is not supported');
|
|
35
|
+
return textDecoder.decode(data);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const entry = (fullPath, root) => {
|
|
39
|
+
const stat = zero.fs.statSync(fullPath);
|
|
40
|
+
return {
|
|
41
|
+
path: fullPath,
|
|
42
|
+
relativePath: zero.path.relative(root, fullPath).replace(/\\/g, '/'),
|
|
43
|
+
type: stat.isDirectory() ? 'directory' : 'file',
|
|
44
|
+
size: stat.size
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const walk = (root, options = {}) => {
|
|
49
|
+
const depth = options.depth ?? Infinity;
|
|
50
|
+
const filesOnly = options.filesOnly === true;
|
|
51
|
+
const out = [];
|
|
52
|
+
const visit = (dir, level) => {
|
|
53
|
+
if (level > depth) return;
|
|
54
|
+
for (const name of zero.fs.readdirSync(dir)) {
|
|
55
|
+
const full = zero.path.join(dir, name);
|
|
56
|
+
const item = entry(full, root);
|
|
57
|
+
if (!filesOnly || item.type === 'file') out.push(item);
|
|
58
|
+
if (item.type === 'directory') visit(full, level + 1);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
visit(root, 1);
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const wildcard = (pattern) => new RegExp('^' + String(pattern)
|
|
66
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
67
|
+
.replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
|
|
68
|
+
|
|
69
|
+
const numbered = (lines, startIndex) => lines
|
|
70
|
+
.map((line, index) => String(startIndex + index + 1).padStart(4, ' ') + '\t' + line)
|
|
71
|
+
.join('\n');
|
|
72
|
+
|
|
73
|
+
const listDirectory = (args, env) => {
|
|
74
|
+
const root = resolveAllowed(args.path, env);
|
|
75
|
+
const depth = Math.max(1, Math.min(args.depth || 2, 5));
|
|
76
|
+
return { ok: true, path: root, depth, entries: walk(root, { depth }) };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const getFileInfo = (args, env) => {
|
|
80
|
+
const target = resolveAllowed(args.path, env);
|
|
81
|
+
const stat = zero.fs.statSync(target);
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
path: target,
|
|
85
|
+
type: stat.isDirectory() ? 'directory' : 'file',
|
|
86
|
+
size: stat.size,
|
|
87
|
+
createdAt: stat.birthtime.toISOString(),
|
|
88
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
89
|
+
lineCount: stat.isFile() ? readUtf8(target).split(/\r?\n/).length : null
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const readFiles = (args = {}, env) => {
|
|
94
|
+
const requests = Array.isArray(args.files) && args.files.length
|
|
95
|
+
? args.files.map((item) => ({ path: item.path, offset: item.offset, length: item.length }))
|
|
96
|
+
: (Array.isArray(args.paths) ? args.paths : []).map((inputPath) => ({ path: inputPath, offset: args.offset, length: args.length }));
|
|
97
|
+
if (!requests.length) throw err('INVALID_PATHS', 'paths or files must contain at least one file');
|
|
98
|
+
if (requests.length > 50) throw err('TOO_MANY_PATHS', 'readFiles supports up to 50 files');
|
|
99
|
+
const files = requests.map((request) => {
|
|
100
|
+
try { return zero.readFileTool.readFile(request, { env }); }
|
|
101
|
+
catch (error) { return { ok: false, path: String(request.path), error: { code: error.code || 'READ_FAILED', message: error.message || String(error) } }; }
|
|
102
|
+
});
|
|
103
|
+
const succeeded = files.filter((item) => item.ok).length;
|
|
104
|
+
return { ok: succeeded === files.length, count: files.length, succeeded, failed: files.length - succeeded, files };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const writeFile = (args, env) => {
|
|
108
|
+
const file = resolveAllowed(args.path, env);
|
|
109
|
+
const mode = args.mode === 'append' ? 'append' : 'overwrite';
|
|
110
|
+
zero.fs.mkdirSync(zero.path.dirname(file), { recursive: true });
|
|
111
|
+
const data = String(args.content ?? '');
|
|
112
|
+
if (mode === 'append') zero.fs.appendFileSync(file, data, 'utf8');
|
|
113
|
+
else zero.fs.writeFileSync(file, data, 'utf8');
|
|
114
|
+
return { ok: true, path: file, mode, bytes: Buffer.byteLength(data) };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const replaceFile = (args, env) => {
|
|
118
|
+
const file = resolveAllowed(args.path, env);
|
|
119
|
+
const oldString = String(args.oldString ?? '');
|
|
120
|
+
if (!oldString) throw err('INVALID_REPLACE', 'oldString must not be empty');
|
|
121
|
+
const newString = String(args.newString ?? '');
|
|
122
|
+
const text = readUtf8(file);
|
|
123
|
+
const replacements = text.split(oldString).length - 1;
|
|
124
|
+
if (Number.isInteger(args.expectedReplacements) && replacements !== args.expectedReplacements) {
|
|
125
|
+
throw err('REPLACE_COUNT_MISMATCH', `expected ${args.expectedReplacements} replacements, got ${replacements}`);
|
|
126
|
+
}
|
|
127
|
+
zero.fs.writeFileSync(file, text.split(oldString).join(newString), 'utf8');
|
|
128
|
+
return { ok: true, path: file, replacements };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const createDirectory = (args, env) => {
|
|
132
|
+
const dir = resolveAllowed(args.path, env);
|
|
133
|
+
zero.fs.mkdirSync(dir, { recursive: true });
|
|
134
|
+
return { ok: true, path: dir, created: zero.fs.existsSync(dir) && zero.fs.statSync(dir).isDirectory() };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const deleteFile = (args, env) => {
|
|
138
|
+
const file = resolveAllowed(args.path, env);
|
|
139
|
+
if (!zero.fs.existsSync(file)) throw err('FILE_NOT_FOUND', 'file does not exist');
|
|
140
|
+
const stat = zero.fs.statSync(file);
|
|
141
|
+
if (!stat.isFile()) throw err('NOT_A_FILE', 'deleteFile supports files only');
|
|
142
|
+
zero.fs.unlinkSync(file);
|
|
143
|
+
return { ok: true, path: file, deleted: true };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const globFiles = (args, env) => {
|
|
147
|
+
const root = resolveAllowed(args.path, env);
|
|
148
|
+
const rx = wildcard(args.pattern || '*');
|
|
149
|
+
const max = Math.max(1, Math.min(args.maxResults || 200, 1000));
|
|
150
|
+
const files = walk(root, { filesOnly: true })
|
|
151
|
+
.filter((item) => rx.test(zero.path.basename(item.path)) || rx.test(item.relativePath))
|
|
152
|
+
.slice(0, max);
|
|
153
|
+
return { ok: true, path: root, pattern: args.pattern, files };
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const grepFiles = (args, env) => {
|
|
157
|
+
const root = resolveAllowed(args.path, env);
|
|
158
|
+
const max = Math.max(1, Math.min(args.maxResults || 200, 1000));
|
|
159
|
+
const rx = args.literal ? null : new RegExp(String(args.pattern), 'i');
|
|
160
|
+
const fileRx = args.filePattern ? wildcard(args.filePattern) : null;
|
|
161
|
+
const matches = [];
|
|
162
|
+
for (const file of walk(root, { filesOnly: true })) {
|
|
163
|
+
if (fileRx && !fileRx.test(zero.path.basename(file.path)) && !fileRx.test(file.relativePath)) continue;
|
|
164
|
+
let text;
|
|
165
|
+
try { text = readUtf8(file.path); } catch { continue; }
|
|
166
|
+
const lines = text.split(/\r?\n/);
|
|
167
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
168
|
+
const line = lines[index];
|
|
169
|
+
const hit = args.literal ? line.includes(String(args.pattern)) : rx.test(line);
|
|
170
|
+
if (!hit) continue;
|
|
171
|
+
matches.push({ path: file.path, relativePath: file.relativePath, line: index + 1, text: line });
|
|
172
|
+
if (matches.length >= max) return { ok: true, path: root, pattern: args.pattern, matches };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { ok: true, path: root, pattern: args.pattern, matches };
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handlers = { readFile: (args, env) => zero.readFileTool.readFile(args, { env }), readFiles, writeFile, replaceFile, createDirectory, deleteFile, listDirectory, getFileInfo, globFiles, grepFiles };
|
|
179
|
+
|
|
180
|
+
const schemaObject = (properties, required = []) => ({ type: 'object', properties, required });
|
|
181
|
+
const toolSchemas = [
|
|
182
|
+
{ name: 'listDirectory', description: 'List files and folders inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' }, depth: { type: 'number' } }, ['path']) },
|
|
183
|
+
{ name: 'getFileInfo', description: 'Get metadata for a file or directory inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' } }, ['path']) },
|
|
184
|
+
{ name: 'readFile', description: zero.readFileTool.descriptor.description, inputSchema: zero.readFileTool.descriptor.inputSchema },
|
|
185
|
+
{ name: 'readFiles', description: 'Read multiple UTF-8 text files inside ZERO_AGENT_ROOTS. Accept paths[] or files[].', inputSchema: schemaObject({ paths: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 50 }, files: { type: 'array', minItems: 1, maxItems: 50, items: schemaObject({ path: { type: 'string' }, offset: { type: 'number' }, length: { type: 'number' } }, ['path']) }, offset: { type: 'number' }, length: { type: 'number' } }) },
|
|
186
|
+
{ name: 'writeFile', description: 'Write or append a UTF-8 text file inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' }, content: { type: 'string' }, mode: { type: 'string', enum: ['overwrite', 'append'] } }, ['path', 'content']) },
|
|
187
|
+
{ name: 'replaceFile', description: 'Replace exact text inside a UTF-8 file in ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' }, oldString: { type: 'string' }, newString: { type: 'string' }, expectedReplacements: { type: 'number' } }, ['path', 'oldString', 'newString']) },
|
|
188
|
+
{ name: 'createDirectory', description: 'Create a directory recursively inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' } }, ['path']) },
|
|
189
|
+
{ name: 'deleteFile', description: 'Delete one file inside ZERO_AGENT_ROOTS. Directories are rejected.', inputSchema: schemaObject({ path: { type: 'string' } }, ['path']) },
|
|
190
|
+
{ name: 'globFiles', description: 'Find files by wildcard pattern inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' }, pattern: { type: 'string' }, maxResults: { type: 'number' } }, ['path']) },
|
|
191
|
+
{ name: 'grepFiles', description: 'Search text files inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' }, pattern: { type: 'string' }, literal: { type: 'boolean' }, filePattern: { type: 'string' }, maxResults: { type: 'number' } }, ['path', 'pattern']) }
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
exports.execute = (tool, args = {}, env = process.env) => {
|
|
195
|
+
const cleanTool = String(tool || '').replace(/^zero\.command\./, '').replace(/^command\./, '');
|
|
196
|
+
const handler = handlers[cleanTool];
|
|
197
|
+
if (!handler) throw err('UNKNOWN_AGENT_TOOL', 'unknown or unsafe agent tool: ' + tool);
|
|
198
|
+
return handler(args, env);
|
|
199
|
+
};
|
|
200
|
+
exports.listTools = () => toolSchemas.map((tool) => ({ ...tool }));
|
|
201
|
+
|
|
202
|
+
exports._private = {};
|
|
203
|
+
exports._private.rootsFor = rootsFor;
|
|
204
|
+
exports._private.resolveAllowed = resolveAllowed;
|
|
205
|
+
exports._private.wildcard = wildcard;
|