@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
@@ -1,94 +1,123 @@
1
- 'use strict';
2
-
3
- const crypto = require('node:crypto');
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
- const localTools = require('./local-tools');
7
-
8
- const defaultMcpRoot = () => path.resolve(__dirname, '../..');
9
- const mcpRoot = (env = process.env) => path.resolve(env.ZERO_MCP_ROOT || defaultMcpRoot());
10
- const providersDir = (env = process.env) => path.join(mcpRoot(env), 'providers');
11
-
12
- const readManifest = (file) => {
13
- try {
14
- return JSON.parse(fs.readFileSync(file, 'utf8'));
15
- } catch (error) {
16
- return { error: error.message };
17
- }
18
- };
19
-
20
- const manifestProviders = (env = process.env) => {
21
- const dir = providersDir(env);
22
- if (!fs.existsSync(dir)) return [];
23
- return fs.readdirSync(dir, { withFileTypes: true })
24
- .filter((item) => item.isDirectory())
25
- .map((item) => {
26
- const root = path.join(dir, item.name);
27
- const manifestFile = path.join(root, 'provider.json');
28
- const manifest = fs.existsSync(manifestFile) ? readManifest(manifestFile) : {};
29
- return {
30
- name: item.name,
31
- type: manifest.type || 'unknown',
32
- description: manifest.description || '',
33
- providerRoot: root,
34
- manifestFile,
35
- manifestError: manifest.error || null,
36
- module: manifest.module || null,
37
- command: manifest.command || null,
38
- args: manifest.args || []
39
- };
40
- });
41
- };
42
-
43
- const safeInternalTools = (provider) => {
44
- if (!provider.module) return [];
45
- const modulePath = provider.module.replaceAll('{providerRoot}', provider.providerRoot.replace(/\\/g, '/'));
46
- const resolved = path.resolve(modulePath);
47
- try {
48
- const runtime = require(resolved);
49
- if (typeof runtime.listTools !== 'function') return [];
50
- return runtime.listTools({}).map((tool) => ({
51
- name: tool.name,
52
- description: tool.description || '',
53
- inputSchema: tool.inputSchema || null
54
- }));
55
- } catch (error) {
56
- return [{ name: '__scan_error__', description: error.message, inputSchema: null }];
57
- }
58
- };
59
- const builtInLocalProvider = () => ({
60
- name: 'zero-agent-local',
61
- type: 'internal',
62
- description: 'Built-in safe local filesystem tools exposed by zero-agent.',
63
- status: 'present',
64
- tools: localTools.listTools(),
65
- tool_count: localTools.listTools().length
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const localTools = require('./local-tools');
7
+ const command = require('./command');
8
+ const zero = require('./zero-runtime');
9
+
10
+ const defaultMcpRoot = () => path.resolve(__dirname, '../..');
11
+ const mcpRoot = (env = process.env) => path.resolve(env.ZERO_MCP_ROOT || defaultMcpRoot());
12
+ const providersDir = (env = process.env) => path.join(mcpRoot(env), 'providers');
13
+
14
+ const readManifest = (file) => {
15
+ try {
16
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
17
+ } catch (error) {
18
+ return { error: error.message };
19
+ }
20
+ };
21
+
22
+ const manifestProviders = (env = process.env) => {
23
+ const dir = providersDir(env);
24
+ if (!fs.existsSync(dir)) return [];
25
+ return fs.readdirSync(dir, { withFileTypes: true })
26
+ .filter((item) => item.isDirectory())
27
+ .map((item) => {
28
+ const root = path.join(dir, item.name);
29
+ const manifestFile = path.join(root, 'provider.json');
30
+ const manifest = fs.existsSync(manifestFile) ? readManifest(manifestFile) : {};
31
+ return {
32
+ name: item.name,
33
+ type: manifest.type || 'unknown',
34
+ description: manifest.description || '',
35
+ providerRoot: root,
36
+ manifestFile,
37
+ manifestError: manifest.error || null,
38
+ module: manifest.module || null,
39
+ command: manifest.command || null,
40
+ args: manifest.args || []
41
+ };
42
+ });
43
+ };
44
+
45
+ const safeInternalTools = (provider) => {
46
+ if (!provider.module) return [];
47
+ const modulePath = provider.module.replaceAll('{providerRoot}', provider.providerRoot.replace(/\\/g, '/'));
48
+ const resolved = path.resolve(modulePath);
49
+ try {
50
+ const runtime = require(resolved);
51
+ if (typeof runtime.listTools !== 'function') return [];
52
+ return runtime.listTools({}).map((tool) => ({
53
+ name: tool.name,
54
+ description: tool.description || '',
55
+ inputSchema: tool.inputSchema || null
56
+ }));
57
+ } catch (error) {
58
+ return [{ name: '__scan_error__', description: error.message, inputSchema: null }];
59
+ }
60
+ };
61
+ const publicCommandTool = (tool) => ({
62
+ name: tool.name,
63
+ description: tool.description || '',
64
+ canonicalName: tool.name,
65
+ category: tool.category,
66
+ function: tool.function,
67
+ aliases: tool.aliases || [],
68
+ inputSchema: tool.inputSchema || null
66
69
  });
67
70
 
68
- const buildCapabilities = (env = process.env) => {
69
- const providers = [builtInLocalProvider(), ...manifestProviders(env).map((provider) => {
70
- const tools = provider.type === 'internal' ? safeInternalTools(provider) : [];
71
- return {
72
- name: provider.name,
73
- type: provider.type,
74
- description: provider.description,
75
- status: provider.manifestError ? 'invalid' : 'present',
76
- tools,
77
- tool_count: tools.length,
78
- mcp_stdio_live_scan: provider.type === 'mcp-stdio' ? false : undefined
79
- };
80
- })];
81
- const payload = {
82
- agent_version: require('../package.json').version,
83
- mcp_root: mcpRoot(env),
84
- providers
71
+ const builtInLocalProvider = () => {
72
+ const tools = command.listTools().map(publicCommandTool);
73
+ return {
74
+ name: 'zero-agent-command',
75
+ type: 'internal',
76
+ description: 'Built-in safe command tools exposed by zero-agent.',
77
+ status: 'present',
78
+ tools,
79
+ tool_count: tools.length
85
80
  };
86
- payload.capability_hash = crypto
87
- .createHash('sha256')
88
- .update(JSON.stringify(payload.providers))
89
- .digest('hex');
90
- return payload;
91
- };
92
-
93
- const capabilities = { mcpRoot, providersDir, manifestProviders, buildCapabilities };
94
- Object.assign(exports, capabilities);
81
+ };
82
+
83
+ const buildCapabilities = (env = process.env) => {
84
+ const providers = [builtInLocalProvider(), ...manifestProviders(env).map((provider) => {
85
+ const tools = provider.type === 'internal' ? safeInternalTools(provider) : [];
86
+ return {
87
+ name: provider.name,
88
+ type: provider.type,
89
+ description: provider.description,
90
+ status: provider.manifestError ? 'invalid' : 'present',
91
+ tools,
92
+ tool_count: tools.length,
93
+ mcp_stdio_live_scan: provider.type === 'mcp-stdio' ? false : undefined
94
+ };
95
+ })];
96
+ const agentVersion = require('../package.json').version;
97
+ const categories = Array.from(new Set(command.listTools().map((tool) => `${tool.provider}.${tool.category}`))).sort();
98
+ const adapters = ['native'];
99
+ const payload = {
100
+ agentVersion,
101
+ agent_version: agentVersion,
102
+ contractVersion: zero.contract.version,
103
+ platform: process.platform,
104
+ architecture: process.arch,
105
+ categories,
106
+ adapters,
107
+ mcp_root: mcpRoot(env),
108
+ providers
109
+ };
110
+ const fingerprint = {
111
+ contractVersion: payload.contractVersion,
112
+ platform: payload.platform,
113
+ architecture: payload.architecture,
114
+ categories: payload.categories,
115
+ adapters: payload.adapters,
116
+ providers: payload.providers
117
+ };
118
+ payload.capability_hash = crypto.createHash('sha256').update(JSON.stringify(fingerprint)).digest('hex');
119
+ return payload;
120
+ };
121
+
122
+ const capabilities = { mcpRoot, providersDir, manifestProviders, buildCapabilities };
123
+ Object.assign(exports, capabilities);
package/src/cli.js CHANGED
@@ -51,8 +51,10 @@ const ensureLocalBootstrap = async (parsed, env) => {
51
51
  console.log('autoInit=OK file=' + init.file);
52
52
  device = config.loadDevice(env);
53
53
  }
54
- const prepared = autostart.prepare({ interval: autostartInterval(parsed) }, env);
55
- console.log('autoSetup=OK launcher=' + prepared.launcherFile);
54
+ const prepared = autostart.isPrepared(env)
55
+ ? { ...autostart.paths(env), ok: true, reused: true }
56
+ : autostart.prepare({ interval: autostartInterval(parsed) }, env);
57
+ console.log('autoSetup=' + (prepared.reused ? 'REUSED' : 'OK') + ' launcher=' + prepared.launcherFile);
56
58
  if (parsed.code && (!device || !device.device_id)) {
57
59
  const paired = await agent.pairDevice({
58
60
  name: parsed.name || device?.name,
@@ -87,7 +89,11 @@ const runSetup = async (parsed, env) => {
87
89
  console.log('Zero Agent Setup');
88
90
  console.log('server=' + server);
89
91
  console.log('name=' + (name || os.hostname()));
90
- runGlobalInstall(parsed, env);
92
+ const installed = runGlobalInstall(parsed, env);
93
+ if (!installed.skipped && !installed.ok) {
94
+ console.error('setup=FAILED global installation did not complete');
95
+ return 70;
96
+ }
91
97
  const init = agent.initDevice({ name, server }, env);
92
98
  console.log('init=OK file=' + init.file);
93
99
  const prepared = autostart.prepare({ interval: autostartInterval(parsed) }, env);
@@ -136,6 +142,21 @@ const runStartOnce = async (env) => {
136
142
  return { heartbeat, capability, task };
137
143
  };
138
144
 
145
+ const runSafeStartCycle = async (env, runOnce = runStartOnce) => {
146
+ try {
147
+ return { ok: true, result: await runOnce(env) };
148
+ } catch (error) {
149
+ return {
150
+ ok: false,
151
+ reconnect: true,
152
+ error: {
153
+ code: error.code || error.cause?.code || 'AGENT_CONNECTION_ERROR',
154
+ message: error.message || String(error)
155
+ }
156
+ };
157
+ }
158
+ };
159
+
139
160
  const printStartBanner = (env) => {
140
161
  const device = config.loadDevice(env);
141
162
  console.log('🚀 Starting Zero Agent...');
@@ -145,26 +166,38 @@ const printStartBanner = (env) => {
145
166
  console.log('🌐 Server: ' + (device?.server || 'https://zero.miru.work'));
146
167
  };
147
168
 
148
- const printLoopResult = (result) => {
169
+ const loopResultEvent = (result) => {
149
170
  if (!result || result.code === 'NOT_PAIRED') {
150
- console.log('❌ Device not paired: run zero-agent pair first');
151
- return;
171
+ return { level: 'error', text: '❌ Device not paired: run zero-agent pair first' };
152
172
  }
153
173
  const hb = result.heartbeat || {};
154
174
  const cap = result.capability || {};
175
+ const task = result.task?.task || null;
176
+ const healthy = Boolean(hb.ok) && Boolean(cap.ok);
177
+ if (healthy && !task) return null;
178
+
155
179
  const device = cap.body?.device || hb.body?.device || null;
156
- const taskState = result.task?.task ? (result.task.task.task_id + ':' + (result.task.task.status || 'done')) : 'none';
157
- console.log(JSON.stringify({
158
- at: new Date().toISOString(),
159
- heartbeat: okMark(Boolean(hb.ok)),
160
- capabilities: okMark(Boolean(cap.ok)),
161
- status: device?.status || null,
162
- name: device?.name || null,
163
- host: device?.host || null,
164
- providers: device?.provider_count ?? null,
165
- last_seen: device?.last_seen || null,
166
- task: taskState
167
- }));
180
+ return {
181
+ level: healthy ? 'info' : 'error',
182
+ text: JSON.stringify({
183
+ at: new Date().toISOString(),
184
+ heartbeat: okMark(Boolean(hb.ok)),
185
+ capabilities: okMark(Boolean(cap.ok)),
186
+ status: device?.status || null,
187
+ name: device?.name || null,
188
+ host: device?.host || null,
189
+ providers: device?.provider_count ?? null,
190
+ last_seen: device?.last_seen || null,
191
+ task: task ? (task.task_id + ':' + (task.status || 'done')) : 'none'
192
+ })
193
+ };
194
+ };
195
+
196
+ const printLoopResult = (result) => {
197
+ const event = loopResultEvent(result);
198
+ if (!event) return;
199
+ if (event.level === 'error') console.error(event.text);
200
+ else console.log(event.text);
168
201
  };
169
202
 
170
203
  async function run(argv = [], env = process.env) {
@@ -225,8 +258,17 @@ async function run(argv = [], env = process.env) {
225
258
  console.log('⏱️ Heartbeat interval: ' + interval + 'ms');
226
259
  console.log('🟢 Device ready; press Ctrl+C to stop');
227
260
  while (true) {
228
- const result = await runStartOnce(env);
229
- printLoopResult(result);
261
+ const cycle = await runSafeStartCycle(env);
262
+ if (cycle.ok) {
263
+ printLoopResult(cycle.result);
264
+ } else {
265
+ console.error(JSON.stringify({
266
+ at: new Date().toISOString(),
267
+ heartbeat: 'FAIL',
268
+ reconnect: 'WAIT',
269
+ error: cycle.error
270
+ }));
271
+ }
230
272
  await new Promise((resolve) => setTimeout(resolve, interval));
231
273
  }
232
274
  }
@@ -235,4 +277,4 @@ async function run(argv = [], env = process.env) {
235
277
  return 64;
236
278
  }
237
279
 
238
- module.exports = { run, usage, status, doctor };
280
+ module.exports = { run, usage, status, doctor, runSafeStartCycle, loopResultEvent };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const zero = require('../../zero-runtime');
6
+ const pathPolicy = require('./path-policy');
7
+
8
+ const descriptor = zero.descriptor.validate({
9
+ name: 'zero.command.filesystem.appendFile',
10
+ aliases: ['zero.command.appendFile'],
11
+ description: 'Append UTF-8 text content to a file inside allowed roots.',
12
+ capability: 'filesystem.write',
13
+ risk: 'write',
14
+ execution: 'target',
15
+ adapters: ['native'],
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: { path: { type: 'string' }, content: { type: 'string' } },
19
+ required: ['path', 'content'],
20
+ additionalProperties: false
21
+ }
22
+ });
23
+
24
+ const appendFile = (args = {}, context = {}) => {
25
+ const file = pathPolicy.resolveAllowed(args.path, context);
26
+ const data = String(args.content ?? '');
27
+ fs.mkdirSync(path.dirname(file), { recursive: true });
28
+ fs.appendFileSync(file, data, 'utf8');
29
+ return { ok: true, path: file, mode: 'append', bytes: Buffer.byteLength(data) };
30
+ };
31
+
32
+ const appendFileModule = { descriptor, appendFile };
33
+ Object.assign(exports, appendFileModule);
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const zero = require('../../zero-runtime');
5
+ const pathPolicy = require('./path-policy');
6
+
7
+ const descriptor = zero.descriptor.validate({
8
+ name: 'zero.command.filesystem.copyDirectory',
9
+ aliases: ['zero.command.copyDirectory'],
10
+ description: 'Copy a directory recursively inside allowed roots.',
11
+ capability: 'filesystem.write',
12
+ risk: 'write',
13
+ execution: 'target',
14
+ adapters: ['native'],
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: { source: { type: 'string' }, destination: { type: 'string' }, overwrite: { type: 'boolean', default: false } },
18
+ required: ['source', 'destination'],
19
+ additionalProperties: false
20
+ }
21
+ });
22
+
23
+ const fail = (code, message) => Object.assign(new Error(message), { code });
24
+ const copyDirectory = (args = {}, context = {}) => {
25
+ const source = pathPolicy.resolveAllowed(args.source, context);
26
+ const destination = pathPolicy.resolveAllowed(args.destination, context);
27
+ if (!fs.statSync(source).isDirectory()) throw fail('NOT_A_DIRECTORY', 'source is not a directory');
28
+ fs.cpSync(source, destination, { recursive: true, force: args.overwrite === true, errorOnExist: args.overwrite !== true });
29
+ return { ok: true, source, destination };
30
+ };
31
+
32
+ const copyDirectoryModule = { descriptor, copyDirectory };
33
+ Object.assign(exports, copyDirectoryModule);
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const zero = require('../../zero-runtime');
6
+ const pathPolicy = require('./path-policy');
7
+
8
+ const descriptor = zero.descriptor.validate({
9
+ name: 'zero.command.filesystem.copyFile',
10
+ aliases: ['zero.command.copyFile'],
11
+ description: 'Copy one file inside allowed roots.',
12
+ capability: 'filesystem.write',
13
+ risk: 'write',
14
+ execution: 'target',
15
+ adapters: ['native'],
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: { source: { type: 'string' }, destination: { type: 'string' } },
19
+ required: ['source', 'destination'],
20
+ additionalProperties: false
21
+ }
22
+ });
23
+
24
+ const copyFile = (args = {}, context = {}) => {
25
+ const source = pathPolicy.resolveAllowed(args.source, context);
26
+ const destination = pathPolicy.resolveAllowed(args.destination, context);
27
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
28
+ fs.copyFileSync(source, destination);
29
+ return { ok: true, source, destination };
30
+ };
31
+
32
+ const copyFileModule = { descriptor, copyFile };
33
+ Object.assign(exports, copyFileModule);
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const zero = require('../../zero-runtime');
5
+ const pathPolicy = require('./path-policy');
6
+
7
+ const descriptor = zero.descriptor.validate({
8
+ name: 'zero.command.filesystem.createDirectory',
9
+ aliases: ['zero.command.createDirectory'],
10
+ description: 'Create a directory recursively inside allowed roots.',
11
+ capability: 'filesystem.write',
12
+ risk: 'write',
13
+ execution: 'target',
14
+ adapters: ['native'],
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: { path: { type: 'string' } },
18
+ required: ['path'],
19
+ additionalProperties: false
20
+ }
21
+ });
22
+
23
+ const createDirectory = (args = {}, context = {}) => {
24
+ const dir = pathPolicy.resolveAllowed(args.path, context);
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ return { ok: true, path: dir, created: fs.existsSync(dir) && fs.statSync(dir).isDirectory() };
27
+ };
28
+
29
+ const createDirectoryModule = { descriptor, createDirectory };
30
+ Object.assign(exports, createDirectoryModule);
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const zero = require('../../zero-runtime');
6
+ const pathPolicy = require('./path-policy');
7
+
8
+ const descriptor = zero.descriptor.validate({
9
+ name: 'zero.command.filesystem.createFile',
10
+ aliases: ['zero.command.createFile'],
11
+ description: 'Create a UTF-8 text file inside allowed roots.',
12
+ capability: 'filesystem.write',
13
+ risk: 'write',
14
+ execution: 'target',
15
+ adapters: ['native'],
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: { path: { type: 'string' }, content: { type: 'string', default: '' }, overwrite: { type: 'boolean', default: false } },
19
+ required: ['path'],
20
+ additionalProperties: false
21
+ }
22
+ });
23
+
24
+ const fail = (code, message) => Object.assign(new Error(message), { code });
25
+ const createFile = (args = {}, context = {}) => {
26
+ const file = pathPolicy.resolveAllowed(args.path, context);
27
+ if (fs.existsSync(file) && args.overwrite !== true) throw fail('FILE_EXISTS', 'file already exists');
28
+ const data = String(args.content ?? '');
29
+ fs.mkdirSync(path.dirname(file), { recursive: true });
30
+ fs.writeFileSync(file, data, 'utf8');
31
+ return { ok: true, path: file, created: true, bytes: Buffer.byteLength(data) };
32
+ };
33
+
34
+ const createFileModule = { descriptor, createFile };
35
+ Object.assign(exports, createFileModule);
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const zero = require('../../zero-runtime');
5
+ const pathPolicy = require('./path-policy');
6
+
7
+ const descriptor = zero.descriptor.validate({
8
+ name: 'zero.command.filesystem.deleteDirectory',
9
+ aliases: ['zero.command.deleteDirectory'],
10
+ description: 'Delete a directory inside allowed roots.',
11
+ capability: 'filesystem.write',
12
+ risk: 'write',
13
+ execution: 'target',
14
+ adapters: ['native'],
15
+ inputSchema: { type: 'object', properties: { path: { type: 'string' }, recursive: { type: 'boolean', default: false } }, required: ['path'], additionalProperties: false }
16
+ });
17
+
18
+ const fail = (code, message) => Object.assign(new Error(message), { code });
19
+ const deleteDirectory = (args = {}, context = {}) => {
20
+ const dir = pathPolicy.resolveAllowed(args.path, context);
21
+ if (!fs.statSync(dir).isDirectory()) throw fail('NOT_A_DIRECTORY', 'path is not a directory');
22
+ fs.rmSync(dir, { recursive: args.recursive === true, force: false });
23
+ return { ok: true, path: dir, deleted: true };
24
+ };
25
+
26
+ const deleteDirectoryModule = { descriptor, deleteDirectory };
27
+ Object.assign(exports, deleteDirectoryModule);
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const zero = require('../../zero-runtime');
5
+ const pathPolicy = require('./path-policy');
6
+
7
+ const descriptor = zero.descriptor.validate({
8
+ name: 'zero.command.filesystem.deleteFile',
9
+ aliases: ['zero.command.deleteFile'],
10
+ description: 'Delete one file inside allowed roots. Directories are rejected.',
11
+ capability: 'filesystem.write',
12
+ risk: 'write',
13
+ execution: 'target',
14
+ adapters: ['native'],
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: { path: { type: 'string' } },
18
+ required: ['path'],
19
+ additionalProperties: false
20
+ }
21
+ });
22
+
23
+ const fail = (code, message) => Object.assign(new Error(message), { code });
24
+ const deleteFile = (args = {}, context = {}) => {
25
+ const file = pathPolicy.resolveAllowed(args.path, context);
26
+ if (!fs.existsSync(file)) throw fail('FILE_NOT_FOUND', 'file does not exist');
27
+ if (!fs.statSync(file).isFile()) throw fail('NOT_A_FILE', 'deleteFile supports files only');
28
+ fs.unlinkSync(file);
29
+ return { ok: true, path: file, deleted: true };
30
+ };
31
+
32
+ const deleteFileModule = { descriptor, deleteFile };
33
+ Object.assign(exports, deleteFileModule);
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const zero = require('../../zero-runtime');
5
+ const pathPolicy = require('./path-policy');
6
+
7
+ const descriptor = zero.descriptor.validate({
8
+ name: 'zero.command.filesystem.exists',
9
+ aliases: ['zero.command.exists'],
10
+ description: 'Check whether a path exists inside allowed roots.',
11
+ capability: 'filesystem.read',
12
+ risk: 'read',
13
+ execution: 'target',
14
+ adapters: ['native'],
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: { path: { type: 'string' } },
18
+ required: ['path'],
19
+ additionalProperties: false
20
+ }
21
+ });
22
+
23
+ const exists = (args = {}, context = {}) => {
24
+ const pathValue = pathPolicy.resolveAllowed(args.path, context);
25
+ return { ok: true, path: pathValue, exists: fs.existsSync(pathValue) };
26
+ };
27
+
28
+ const existsModule = { descriptor, exists };
29
+ Object.assign(exports, existsModule);
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const zero = require('../../zero-runtime');
5
+ const pathPolicy = require('./path-policy');
6
+ const text = require('./text');
7
+
8
+ const descriptor = zero.descriptor.validate({
9
+ name: 'zero.command.filesystem.getFileInfo',
10
+ aliases: ['zero.command.getFileInfo'],
11
+ description: 'Get metadata for a file or directory inside allowed roots.',
12
+ capability: 'filesystem.read',
13
+ risk: 'read',
14
+ execution: 'target',
15
+ adapters: ['native'],
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: { path: { type: 'string' } },
19
+ required: ['path'],
20
+ additionalProperties: false
21
+ }
22
+ });
23
+
24
+ const getFileInfo = (args = {}, context = {}) => {
25
+ const target = pathPolicy.resolveAllowed(args.path, context);
26
+ const stat = fs.statSync(target);
27
+ return {
28
+ ok: true,
29
+ path: target,
30
+ type: stat.isDirectory() ? 'directory' : 'file',
31
+ size: stat.size,
32
+ createdAt: stat.birthtime.toISOString(),
33
+ modifiedAt: stat.mtime.toISOString(),
34
+ lineCount: stat.isFile() ? text.readUtf8(target).split(/\r?\n/).length : null
35
+ };
36
+ };
37
+
38
+ const getFileInfoModule = { descriptor, getFileInfo };
39
+ Object.assign(exports, getFileInfoModule);