@miruzero/zero-agent 0.3.4 → 0.3.6

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 (38) hide show
  1. package/README.md +1 -1
  2. package/package.json +9 -2
  3. package/src/agent.js +167 -145
  4. package/src/autostart.js +53 -7
  5. package/src/capabilities.js +155 -89
  6. package/src/chatgpt-auth.js +121 -0
  7. package/src/cli.js +63 -21
  8. package/src/command/filesystem/append-file.js +33 -0
  9. package/src/command/filesystem/copy-directory.js +33 -0
  10. package/src/command/filesystem/copy-file.js +33 -0
  11. package/src/command/filesystem/create-directory.js +30 -0
  12. package/src/command/filesystem/create-file.js +35 -0
  13. package/src/command/filesystem/delete-directory.js +27 -0
  14. package/src/command/filesystem/delete-file.js +33 -0
  15. package/src/command/filesystem/exists.js +29 -0
  16. package/src/command/filesystem/get-file-info.js +39 -0
  17. package/src/command/filesystem/glob-files.js +39 -0
  18. package/src/command/filesystem/grep-files.js +54 -0
  19. package/src/command/filesystem/hash-file.js +35 -0
  20. package/src/command/filesystem/index.js +34 -0
  21. package/src/command/filesystem/list-directory.js +33 -0
  22. package/src/command/filesystem/move-directory.js +30 -0
  23. package/src/command/filesystem/move-file.js +33 -0
  24. package/src/command/filesystem/path-policy.js +31 -0
  25. package/src/command/filesystem/read-file.js +37 -0
  26. package/src/command/filesystem/read-files.js +56 -0
  27. package/src/command/filesystem/rename-file.js +20 -0
  28. package/src/command/filesystem/replace-file.js +45 -0
  29. package/src/command/filesystem/text.js +17 -0
  30. package/src/command/filesystem/touch-file.js +29 -0
  31. package/src/command/filesystem/truncate-file.js +26 -0
  32. package/src/command/filesystem/walk.js +39 -0
  33. package/src/command/filesystem/write-file.js +39 -0
  34. package/src/command/index.js +49 -0
  35. package/src/command/path/index.js +53 -0
  36. package/src/index.js +7 -0
  37. package/src/local-tools.js +205 -171
  38. package/src/zero-runtime.js +13 -0
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const entry = (fullPath, root) => {
7
+ const stat = fs.statSync(fullPath);
8
+ return {
9
+ path: fullPath,
10
+ relativePath: path.relative(root, fullPath).replace(/\\/g, '/'),
11
+ type: stat.isDirectory() ? 'directory' : 'file',
12
+ size: stat.size
13
+ };
14
+ };
15
+
16
+ const walk = (root, options = {}) => {
17
+ const depth = options.depth ?? Infinity;
18
+ const filesOnly = options.filesOnly === true;
19
+ const out = [];
20
+ const visit = (dir, level) => {
21
+ if (level > depth) return;
22
+ for (const name of fs.readdirSync(dir)) {
23
+ const full = path.join(dir, name);
24
+ const item = entry(full, root);
25
+ if (!filesOnly || item.type === 'file') out.push(item);
26
+ if (item.type === 'directory') visit(full, level + 1);
27
+ }
28
+ };
29
+ visit(root, 1);
30
+ return out;
31
+ };
32
+
33
+ const wildcard = (pattern) => new RegExp('^' + String(pattern)
34
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
35
+ .replace(/\*/g, '.*')
36
+ .replace(/\?/g, '.') + '$', 'i');
37
+
38
+ const walkModule = { entry, walk, wildcard };
39
+ Object.assign(exports, walkModule);
@@ -0,0 +1,39 @@
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.writeFile',
10
+ aliases: ['zero.command.writeFile'],
11
+ description: 'Write or append 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: {
19
+ path: { type: 'string' },
20
+ content: { type: 'string' },
21
+ mode: { type: 'string', enum: ['overwrite', 'append'], default: 'overwrite' }
22
+ },
23
+ required: ['path', 'content'],
24
+ additionalProperties: false
25
+ }
26
+ });
27
+
28
+ const writeFile = (args = {}, context = {}) => {
29
+ const file = pathPolicy.resolveAllowed(args.path, context);
30
+ const mode = args.mode === 'append' ? 'append' : 'overwrite';
31
+ const data = String(args.content ?? '');
32
+ fs.mkdirSync(path.dirname(file), { recursive: true });
33
+ if (mode === 'append') fs.appendFileSync(file, data, 'utf8');
34
+ else fs.writeFileSync(file, data, 'utf8');
35
+ return { ok: true, path: file, mode, bytes: Buffer.byteLength(data) };
36
+ };
37
+
38
+ const writeFileModule = { descriptor, writeFile };
39
+ Object.assign(exports, writeFileModule);
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const zero = require('../zero-runtime');
4
+ const filesystem = require('./filesystem');
5
+ const pathTools = require('./path');
6
+
7
+ const fsToolNames = [
8
+ 'readFile', 'readFiles', 'writeFile', 'appendFile', 'createFile', 'touchFile', 'replaceFile',
9
+ 'createDirectory', 'deleteFile', 'copyDirectory', 'moveDirectory', 'deleteDirectory', 'truncateFile',
10
+ 'listDirectory', 'getFileInfo', 'globFiles', 'grepFiles', 'copyFile', 'moveFile', 'renameFile', 'exists', 'hashFile'
11
+ ];
12
+ const pathToolNames = [
13
+ 'resolvePath', 'normalizePath', 'joinPath', 'relativePath', 'getParentDirectory', 'getFilename', 'getExtension'
14
+ ];
15
+
16
+ const descriptors = [
17
+ ...fsToolNames.map((name) => filesystem[`${name}Descriptor`]),
18
+ ...pathToolNames.map((name) => pathTools[`${name}Descriptor`])
19
+ ];
20
+ const catalog = zero.catalog.create(descriptors);
21
+ const handlers = new Map([
22
+ ...fsToolNames.map((name) => [filesystem[`${name}Descriptor`].name, filesystem[name]]),
23
+ ...pathToolNames.map((name) => [pathTools[`${name}Descriptor`].name, pathTools[name]])
24
+ ]);
25
+
26
+ const localAliasMap = {};
27
+ for (const name of fsToolNames) {
28
+ localAliasMap[name] = `zero.command.${name}`;
29
+ localAliasMap[`command.${name}`] = `zero.command.${name}`;
30
+ }
31
+ for (const name of pathToolNames) {
32
+ localAliasMap[name] = `zero.command.${name}`;
33
+ localAliasMap[`command.${name}`] = `zero.command.${name}`;
34
+ }
35
+ const localAlias = (name) => localAliasMap[name] || name;
36
+
37
+ const command = {
38
+ filesystem: Object.fromEntries(fsToolNames.map((name) => [name, filesystem[name]])),
39
+ path: Object.fromEntries(pathToolNames.map((name) => [name, pathTools[name]])),
40
+ listTools: (filter = {}) => catalog.list(filter),
41
+ resolveTool: (name) => catalog.resolve(localAlias(name)),
42
+ callTool: (name, args = {}, context = {}) => {
43
+ const descriptor = catalog.resolve(localAlias(name));
44
+ const handler = handlers.get(descriptor.name);
45
+ return handler(args, context);
46
+ }
47
+ };
48
+
49
+ Object.assign(exports, command);
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const zero = require('../../zero-runtime');
5
+
6
+ const descriptor = (name, description, properties, required = []) => zero.descriptor.validate({
7
+ name: `zero.command.path.${name}`,
8
+ aliases: [`zero.command.${name}`],
9
+ description,
10
+ capability: 'path.compute',
11
+ risk: 'read',
12
+ execution: 'target',
13
+ adapters: ['native'],
14
+ inputSchema: { type: 'object', properties, required, additionalProperties: false }
15
+ });
16
+
17
+ const resolvePathDescriptor = descriptor('resolvePath', 'Resolve a path against an optional base path.', {
18
+ path: { type: 'string' }, base: { type: 'string' }
19
+ }, ['path']);
20
+ const normalizePathDescriptor = descriptor('normalizePath', 'Normalize a path string for the target platform.', {
21
+ path: { type: 'string' }
22
+ }, ['path']);
23
+ const joinPathDescriptor = descriptor('joinPath', 'Join path segments for the target platform.', {
24
+ segments: { type: 'array', items: { type: 'string' }, minItems: 1 }
25
+ }, ['segments']);
26
+ const relativePathDescriptor = descriptor('relativePath', 'Compute a relative path between two paths.', {
27
+ from: { type: 'string' }, to: { type: 'string' }
28
+ }, ['from', 'to']);
29
+ const getParentDirectoryDescriptor = descriptor('getParentDirectory', 'Return the parent directory name for a path.', {
30
+ path: { type: 'string' }
31
+ }, ['path']);
32
+ const getFilenameDescriptor = descriptor('getFilename', 'Return the filename portion of a path.', {
33
+ path: { type: 'string' }, stripExtension: { type: 'boolean', default: false }
34
+ }, ['path']);
35
+ const getExtensionDescriptor = descriptor('getExtension', 'Return the extension portion of a path.', {
36
+ path: { type: 'string' }
37
+ }, ['path']);
38
+
39
+ const asText = (value) => String(value ?? '');
40
+ const resolvePath = (args = {}) => ({ ok: true, path: args.base ? path.resolve(asText(args.base), asText(args.path)) : path.resolve(asText(args.path)) });
41
+ const normalizePath = (args = {}) => ({ ok: true, path: path.normalize(asText(args.path)) });
42
+ const joinPath = (args = {}) => ({ ok: true, path: path.join(...(Array.isArray(args.segments) ? args.segments.map(asText) : [])) });
43
+ const relativePath = (args = {}) => ({ ok: true, path: path.relative(asText(args.from), asText(args.to)) });
44
+ const getParentDirectory = (args = {}) => ({ ok: true, path: path.dirname(asText(args.path)) });
45
+ const getFilename = (args = {}) => ({ ok: true, name: path.basename(asText(args.path), args.stripExtension ? path.extname(asText(args.path)) : undefined) });
46
+ const getExtension = (args = {}) => ({ ok: true, extension: path.extname(asText(args.path)) });
47
+
48
+ const pathTools = {
49
+ resolvePathDescriptor, normalizePathDescriptor, joinPathDescriptor, relativePathDescriptor,
50
+ getParentDirectoryDescriptor, getFilenameDescriptor, getExtensionDescriptor,
51
+ resolvePath, normalizePath, joinPath, relativePath, getParentDirectory, getFilename, getExtension
52
+ };
53
+ Object.assign(exports, pathTools);
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const command = require('./command');
4
+
5
+ const zeroAgent = { command };
6
+
7
+ Object.assign(exports, zeroAgent);
@@ -1,171 +1,205 @@
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 textDecoder = new TextDecoder('utf-8', { fatal: false });
8
- const err = (code, message) => Object.assign(new Error(message), { code });
9
- const normalize = (value) => path.resolve(String(value)).toLowerCase();
10
-
11
- const rootsFor = (env = process.env) => {
12
- const configured = String(env.ZERO_AGENT_ROOTS || '')
13
- .split(';')
14
- .map((item) => item.trim())
15
- .filter(Boolean);
16
- const roots = configured.length ? configured : [os.homedir(), process.cwd()];
17
- return roots.map((item) => path.resolve(item));
18
- };
19
-
20
- const resolveAllowed = (inputPath, env = process.env) => {
21
- if (!inputPath || typeof inputPath !== 'string') throw err('INVALID_PATH', 'path is required');
22
- const roots = rootsFor(env);
23
- const target = path.resolve(inputPath);
24
- const targetNorm = normalize(target);
25
- const ok = roots.some((root) => {
26
- const rootNorm = normalize(root);
27
- return targetNorm === rootNorm || targetNorm.startsWith(rootNorm + path.sep.toLowerCase());
28
- });
29
- if (!ok) throw err('PATH_NOT_ALLOWED', 'path is outside agent allowed roots');
30
- return target;
31
- };
32
-
33
- const readUtf8 = (file) => {
34
- const data = fs.readFileSync(file);
35
- if (data.includes(0)) throw err('BINARY_FILE', 'binary file is not supported');
36
- return textDecoder.decode(data);
37
- };
38
-
39
- const entry = (fullPath, root) => {
40
- const stat = fs.statSync(fullPath);
41
- return {
42
- path: fullPath,
43
- relativePath: path.relative(root, fullPath).replace(/\\/g, '/'),
44
- type: stat.isDirectory() ? 'directory' : 'file',
45
- size: stat.size
46
- };
47
- };
48
-
49
- const walk = (root, options = {}) => {
50
- const depth = options.depth ?? Infinity;
51
- const filesOnly = options.filesOnly === true;
52
- const out = [];
53
- const visit = (dir, level) => {
54
- if (level > depth) return;
55
- for (const name of fs.readdirSync(dir)) {
56
- const full = path.join(dir, name);
57
- const item = entry(full, root);
58
- if (!filesOnly || item.type === 'file') out.push(item);
59
- if (item.type === 'directory') visit(full, level + 1);
60
- }
61
- };
62
- visit(root, 1);
63
- return out;
64
- };
65
-
66
- const wildcard = (pattern) => new RegExp('^' + String(pattern)
67
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
68
- .replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
69
-
70
- const numbered = (lines, startIndex) => lines
71
- .map((line, index) => String(startIndex + index + 1).padStart(4, ' ') + '\t' + line)
72
- .join('\n');
73
-
74
- const listDirectory = (args, env) => {
75
- const root = resolveAllowed(args.path, env);
76
- const depth = Math.max(1, Math.min(args.depth || 2, 5));
77
- return { ok: true, path: root, depth, entries: walk(root, { depth }) };
78
- };
79
-
80
- const getFileInfo = (args, env) => {
81
- const target = resolveAllowed(args.path, env);
82
- const stat = fs.statSync(target);
83
- return {
84
- ok: true,
85
- path: target,
86
- type: stat.isDirectory() ? 'directory' : 'file',
87
- size: stat.size,
88
- createdAt: stat.birthtime.toISOString(),
89
- modifiedAt: stat.mtime.toISOString(),
90
- lineCount: stat.isFile() ? readUtf8(target).split(/\r?\n/).length : null
91
- };
92
- };
93
-
94
- const readFile = (args, env) => {
95
- const file = resolveAllowed(args.path, env);
96
- const lines = readUtf8(file).split(/\r?\n/);
97
- const offset = Number.isInteger(args.offset) ? args.offset : 0;
98
- const length = Number.isInteger(args.length) ? args.length : 200;
99
- const start = offset < 0 ? Math.max(lines.length + offset, 0) : Math.max(offset, 0);
100
- const slice = lines.slice(start, start + length);
101
- return { ok: true, path: file, totalLines: lines.length, offset: start, length: slice.length, content: numbered(slice, start) };
102
- };
103
-
104
- const readFiles = (args = {}, env) => {
105
- const requests = Array.isArray(args.files) && args.files.length
106
- ? args.files.map((item) => ({ path: item.path, offset: item.offset, length: item.length }))
107
- : (Array.isArray(args.paths) ? args.paths : []).map((inputPath) => ({ path: inputPath, offset: args.offset, length: args.length }));
108
- if (!requests.length) throw err('INVALID_PATHS', 'paths or files must contain at least one file');
109
- if (requests.length > 50) throw err('TOO_MANY_PATHS', 'readFiles supports up to 50 files');
110
- const files = requests.map((request) => {
111
- try { return readFile(request, env); }
112
- catch (error) { return { ok: false, path: String(request.path), error: { code: error.code || 'READ_FAILED', message: error.message || String(error) } }; }
113
- });
114
- const succeeded = files.filter((item) => item.ok).length;
115
- return { ok: succeeded === files.length, count: files.length, succeeded, failed: files.length - succeeded, files };
116
- };
117
-
118
- const globFiles = (args, env) => {
119
- const root = resolveAllowed(args.path, env);
120
- const rx = wildcard(args.pattern || '*');
121
- const max = Math.max(1, Math.min(args.maxResults || 200, 1000));
122
- const files = walk(root, { filesOnly: true })
123
- .filter((item) => rx.test(path.basename(item.path)) || rx.test(item.relativePath))
124
- .slice(0, max);
125
- return { ok: true, path: root, pattern: args.pattern, files };
126
- };
127
-
128
- const grepFiles = (args, env) => {
129
- const root = resolveAllowed(args.path, env);
130
- const max = Math.max(1, Math.min(args.maxResults || 200, 1000));
131
- const rx = args.literal ? null : new RegExp(String(args.pattern), 'i');
132
- const fileRx = args.filePattern ? wildcard(args.filePattern) : null;
133
- const matches = [];
134
- for (const file of walk(root, { filesOnly: true })) {
135
- if (fileRx && !fileRx.test(path.basename(file.path)) && !fileRx.test(file.relativePath)) continue;
136
- let text;
137
- try { text = readUtf8(file.path); } catch { continue; }
138
- const lines = text.split(/\r?\n/);
139
- for (let index = 0; index < lines.length; index += 1) {
140
- const line = lines[index];
141
- const hit = args.literal ? line.includes(String(args.pattern)) : rx.test(line);
142
- if (!hit) continue;
143
- matches.push({ path: file.path, relativePath: file.relativePath, line: index + 1, text: line });
144
- if (matches.length >= max) return { ok: true, path: root, pattern: args.pattern, matches };
145
- }
146
- }
147
- return { ok: true, path: root, pattern: args.pattern, matches };
148
- };
149
-
150
- const handlers = { listDirectory, getFileInfo, readFile, readFiles, globFiles, grepFiles };
151
-
152
- const schemaObject = (properties, required = []) => ({ type: 'object', properties, required });
153
- const toolSchemas = [
154
- { name: 'listDirectory', description: 'List files and folders inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' }, depth: { type: 'number' } }, ['path']) },
155
- { name: 'getFileInfo', description: 'Get metadata for a file or directory inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' } }, ['path']) },
156
- { name: 'readFile', description: 'Read a UTF-8 text file inside ZERO_AGENT_ROOTS with line pagination.', inputSchema: schemaObject({ path: { type: 'string' }, offset: { type: 'number' }, length: { type: 'number' } }, ['path']) },
157
- { 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' } }) },
158
- { name: 'globFiles', description: 'Find files by wildcard pattern inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' }, pattern: { type: 'string' }, maxResults: { type: 'number' } }, ['path']) },
159
- { 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']) }
160
- ];
161
-
162
- const execute = (tool, args = {}, env = process.env) => {
163
- const cleanTool = String(tool || '').replace(/^command\./, '');
164
- const handler = handlers[cleanTool];
165
- if (!handler) throw err('UNKNOWN_AGENT_TOOL', 'unknown or unsafe agent tool: ' + tool);
166
- return handler(args, env);
167
- };
168
- const listTools = () => toolSchemas.map((tool) => ({ ...tool }));
169
-
170
- const localTools = { execute, listTools, _private: { rootsFor, resolveAllowed, wildcard } };
171
- Object.assign(exports, localTools);
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const readFileModule = require('./command/filesystem/read-file');
7
+
8
+ const textDecoder = new TextDecoder('utf-8', { fatal: false });
9
+ const err = (code, message) => Object.assign(new Error(message), { code });
10
+ const normalize = (value) => path.resolve(String(value)).toLowerCase();
11
+
12
+ const rootsFor = (env = process.env) => {
13
+ const configured = String(env.ZERO_AGENT_ROOTS || '')
14
+ .split(';')
15
+ .map((item) => item.trim())
16
+ .filter(Boolean);
17
+ const roots = configured.length ? configured : [os.homedir(), process.cwd()];
18
+ return roots.map((item) => path.resolve(item));
19
+ };
20
+
21
+ const resolveAllowed = (inputPath, env = process.env) => {
22
+ if (!inputPath || typeof inputPath !== 'string') throw err('INVALID_PATH', 'path is required');
23
+ const roots = rootsFor(env);
24
+ const target = path.resolve(inputPath);
25
+ const targetNorm = normalize(target);
26
+ const ok = roots.some((root) => {
27
+ const rootNorm = normalize(root);
28
+ return targetNorm === rootNorm || targetNorm.startsWith(rootNorm + path.sep.toLowerCase());
29
+ });
30
+ if (!ok) throw err('PATH_NOT_ALLOWED', 'path is outside agent allowed roots');
31
+ return target;
32
+ };
33
+
34
+ const readUtf8 = (file) => {
35
+ const data = fs.readFileSync(file);
36
+ if (data.includes(0)) throw err('BINARY_FILE', 'binary file is not supported');
37
+ return textDecoder.decode(data);
38
+ };
39
+
40
+ const entry = (fullPath, root) => {
41
+ const stat = fs.statSync(fullPath);
42
+ return {
43
+ path: fullPath,
44
+ relativePath: path.relative(root, fullPath).replace(/\\/g, '/'),
45
+ type: stat.isDirectory() ? 'directory' : 'file',
46
+ size: stat.size
47
+ };
48
+ };
49
+
50
+ const walk = (root, options = {}) => {
51
+ const depth = options.depth ?? Infinity;
52
+ const filesOnly = options.filesOnly === true;
53
+ const out = [];
54
+ const visit = (dir, level) => {
55
+ if (level > depth) return;
56
+ for (const name of fs.readdirSync(dir)) {
57
+ const full = path.join(dir, name);
58
+ const item = entry(full, root);
59
+ if (!filesOnly || item.type === 'file') out.push(item);
60
+ if (item.type === 'directory') visit(full, level + 1);
61
+ }
62
+ };
63
+ visit(root, 1);
64
+ return out;
65
+ };
66
+
67
+ const wildcard = (pattern) => new RegExp('^' + String(pattern)
68
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
69
+ .replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
70
+
71
+ const numbered = (lines, startIndex) => lines
72
+ .map((line, index) => String(startIndex + index + 1).padStart(4, ' ') + '\t' + line)
73
+ .join('\n');
74
+
75
+ const listDirectory = (args, env) => {
76
+ const root = resolveAllowed(args.path, env);
77
+ const depth = Math.max(1, Math.min(args.depth || 2, 5));
78
+ return { ok: true, path: root, depth, entries: walk(root, { depth }) };
79
+ };
80
+
81
+ const getFileInfo = (args, env) => {
82
+ const target = resolveAllowed(args.path, env);
83
+ const stat = fs.statSync(target);
84
+ return {
85
+ ok: true,
86
+ path: target,
87
+ type: stat.isDirectory() ? 'directory' : 'file',
88
+ size: stat.size,
89
+ createdAt: stat.birthtime.toISOString(),
90
+ modifiedAt: stat.mtime.toISOString(),
91
+ lineCount: stat.isFile() ? readUtf8(target).split(/\r?\n/).length : null
92
+ };
93
+ };
94
+
95
+ const readFiles = (args = {}, env) => {
96
+ const requests = Array.isArray(args.files) && args.files.length
97
+ ? args.files.map((item) => ({ path: item.path, offset: item.offset, length: item.length }))
98
+ : (Array.isArray(args.paths) ? args.paths : []).map((inputPath) => ({ path: inputPath, offset: args.offset, length: args.length }));
99
+ if (!requests.length) throw err('INVALID_PATHS', 'paths or files must contain at least one file');
100
+ if (requests.length > 50) throw err('TOO_MANY_PATHS', 'readFiles supports up to 50 files');
101
+ const files = requests.map((request) => {
102
+ try { return readFileModule.readFile(request, { env }); }
103
+ catch (error) { return { ok: false, path: String(request.path), error: { code: error.code || 'READ_FAILED', message: error.message || String(error) } }; }
104
+ });
105
+ const succeeded = files.filter((item) => item.ok).length;
106
+ return { ok: succeeded === files.length, count: files.length, succeeded, failed: files.length - succeeded, files };
107
+ };
108
+
109
+ const writeFile = (args, env) => {
110
+ const file = resolveAllowed(args.path, env);
111
+ const mode = args.mode === 'append' ? 'append' : 'overwrite';
112
+ fs.mkdirSync(path.dirname(file), { recursive: true });
113
+ const data = String(args.content ?? '');
114
+ if (mode === 'append') fs.appendFileSync(file, data, 'utf8');
115
+ else fs.writeFileSync(file, data, 'utf8');
116
+ return { ok: true, path: file, mode, bytes: Buffer.byteLength(data) };
117
+ };
118
+
119
+ const replaceFile = (args, env) => {
120
+ const file = resolveAllowed(args.path, env);
121
+ const oldString = String(args.oldString ?? '');
122
+ if (!oldString) throw err('INVALID_REPLACE', 'oldString must not be empty');
123
+ const newString = String(args.newString ?? '');
124
+ const text = readUtf8(file);
125
+ const replacements = text.split(oldString).length - 1;
126
+ if (Number.isInteger(args.expectedReplacements) && replacements !== args.expectedReplacements) {
127
+ throw err('REPLACE_COUNT_MISMATCH', `expected ${args.expectedReplacements} replacements, got ${replacements}`);
128
+ }
129
+ fs.writeFileSync(file, text.split(oldString).join(newString), 'utf8');
130
+ return { ok: true, path: file, replacements };
131
+ };
132
+
133
+ const createDirectory = (args, env) => {
134
+ const dir = resolveAllowed(args.path, env);
135
+ fs.mkdirSync(dir, { recursive: true });
136
+ return { ok: true, path: dir, created: fs.existsSync(dir) && fs.statSync(dir).isDirectory() };
137
+ };
138
+
139
+ const deleteFile = (args, env) => {
140
+ const file = resolveAllowed(args.path, env);
141
+ if (!fs.existsSync(file)) throw err('FILE_NOT_FOUND', 'file does not exist');
142
+ const stat = fs.statSync(file);
143
+ if (!stat.isFile()) throw err('NOT_A_FILE', 'deleteFile supports files only');
144
+ fs.unlinkSync(file);
145
+ return { ok: true, path: file, deleted: true };
146
+ };
147
+
148
+ const globFiles = (args, env) => {
149
+ const root = resolveAllowed(args.path, env);
150
+ const rx = wildcard(args.pattern || '*');
151
+ const max = Math.max(1, Math.min(args.maxResults || 200, 1000));
152
+ const files = walk(root, { filesOnly: true })
153
+ .filter((item) => rx.test(path.basename(item.path)) || rx.test(item.relativePath))
154
+ .slice(0, max);
155
+ return { ok: true, path: root, pattern: args.pattern, files };
156
+ };
157
+
158
+ const grepFiles = (args, env) => {
159
+ const root = resolveAllowed(args.path, env);
160
+ const max = Math.max(1, Math.min(args.maxResults || 200, 1000));
161
+ const rx = args.literal ? null : new RegExp(String(args.pattern), 'i');
162
+ const fileRx = args.filePattern ? wildcard(args.filePattern) : null;
163
+ const matches = [];
164
+ for (const file of walk(root, { filesOnly: true })) {
165
+ if (fileRx && !fileRx.test(path.basename(file.path)) && !fileRx.test(file.relativePath)) continue;
166
+ let text;
167
+ try { text = readUtf8(file.path); } catch { continue; }
168
+ const lines = text.split(/\r?\n/);
169
+ for (let index = 0; index < lines.length; index += 1) {
170
+ const line = lines[index];
171
+ const hit = args.literal ? line.includes(String(args.pattern)) : rx.test(line);
172
+ if (!hit) continue;
173
+ matches.push({ path: file.path, relativePath: file.relativePath, line: index + 1, text: line });
174
+ if (matches.length >= max) return { ok: true, path: root, pattern: args.pattern, matches };
175
+ }
176
+ }
177
+ return { ok: true, path: root, pattern: args.pattern, matches };
178
+ };
179
+
180
+ const handlers = { readFile: (args, env) => readFileModule.readFile(args, { env }), readFiles, writeFile, replaceFile, createDirectory, deleteFile, listDirectory, getFileInfo, globFiles, grepFiles };
181
+
182
+ const schemaObject = (properties, required = []) => ({ type: 'object', properties, required });
183
+ const toolSchemas = [
184
+ { name: 'listDirectory', description: 'List files and folders inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' }, depth: { type: 'number' } }, ['path']) },
185
+ { name: 'getFileInfo', description: 'Get metadata for a file or directory inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' } }, ['path']) },
186
+ { name: 'readFile', description: readFileModule.descriptor.description, inputSchema: readFileModule.descriptor.inputSchema },
187
+ { 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' } }) },
188
+ { 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']) },
189
+ { 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']) },
190
+ { name: 'createDirectory', description: 'Create a directory recursively inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' } }, ['path']) },
191
+ { name: 'deleteFile', description: 'Delete one file inside ZERO_AGENT_ROOTS. Directories are rejected.', inputSchema: schemaObject({ path: { type: 'string' } }, ['path']) },
192
+ { name: 'globFiles', description: 'Find files by wildcard pattern inside ZERO_AGENT_ROOTS.', inputSchema: schemaObject({ path: { type: 'string' }, pattern: { type: 'string' }, maxResults: { type: 'number' } }, ['path']) },
193
+ { 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']) }
194
+ ];
195
+
196
+ const execute = (tool, args = {}, env = process.env) => {
197
+ const cleanTool = String(tool || '').replace(/^zero\.command\./, '').replace(/^command\./, '');
198
+ const handler = handlers[cleanTool];
199
+ if (!handler) throw err('UNKNOWN_AGENT_TOOL', 'unknown or unsafe agent tool: ' + tool);
200
+ return handler(args, env);
201
+ };
202
+ const listTools = () => toolSchemas.map((tool) => ({ ...tool }));
203
+
204
+ const localTools = { execute, listTools, _private: { rootsFor, resolveAllowed, wildcard } };
205
+ Object.assign(exports, localTools);
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const load = () => {
4
+ try {
5
+ return require('@miruzero/zero');
6
+ } catch (error) {
7
+ if (error?.code !== 'MODULE_NOT_FOUND' || !String(error.message).includes('@miruzero/zero')) throw error;
8
+ return require('../../../packages/zero');
9
+ }
10
+ };
11
+
12
+ const zero = load();
13
+ Object.assign(exports, zero);