@miruzero/zero-agent 0.3.4 → 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.
- package/README.md +1 -1
- package/package.json +9 -2
- package/src/agent.js +158 -145
- package/src/autostart.js +53 -7
- package/src/capabilities.js +120 -91
- package/src/cli.js +63 -21
- package/src/command/filesystem/append-file.js +33 -0
- package/src/command/filesystem/copy-directory.js +33 -0
- package/src/command/filesystem/copy-file.js +33 -0
- package/src/command/filesystem/create-directory.js +30 -0
- package/src/command/filesystem/create-file.js +35 -0
- package/src/command/filesystem/delete-directory.js +27 -0
- package/src/command/filesystem/delete-file.js +33 -0
- package/src/command/filesystem/exists.js +29 -0
- package/src/command/filesystem/get-file-info.js +39 -0
- package/src/command/filesystem/glob-files.js +39 -0
- package/src/command/filesystem/grep-files.js +54 -0
- package/src/command/filesystem/hash-file.js +35 -0
- package/src/command/filesystem/index.js +34 -0
- package/src/command/filesystem/list-directory.js +33 -0
- package/src/command/filesystem/move-directory.js +30 -0
- package/src/command/filesystem/move-file.js +33 -0
- package/src/command/filesystem/path-policy.js +31 -0
- package/src/command/filesystem/read-file.js +37 -0
- package/src/command/filesystem/read-files.js +56 -0
- package/src/command/filesystem/rename-file.js +20 -0
- package/src/command/filesystem/replace-file.js +45 -0
- package/src/command/filesystem/text.js +17 -0
- package/src/command/filesystem/touch-file.js +29 -0
- package/src/command/filesystem/truncate-file.js +26 -0
- package/src/command/filesystem/walk.js +39 -0
- package/src/command/filesystem/write-file.js +39 -0
- package/src/command/index.js +49 -0
- package/src/command/path/index.js +53 -0
- package/src/index.js +7 -0
- package/src/local-tools.js +205 -171
- package/src/zero-runtime.js +13 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const zero = require('../../zero-runtime');
|
|
5
|
+
const pathPolicy = require('./path-policy');
|
|
6
|
+
const walk = require('./walk');
|
|
7
|
+
|
|
8
|
+
const descriptor = zero.descriptor.validate({
|
|
9
|
+
name: 'zero.command.filesystem.globFiles',
|
|
10
|
+
aliases: ['zero.command.globFiles'],
|
|
11
|
+
description: 'Find files by wildcard pattern inside allowed roots.',
|
|
12
|
+
capability: 'filesystem.read',
|
|
13
|
+
risk: 'read',
|
|
14
|
+
execution: 'target',
|
|
15
|
+
adapters: ['native'],
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: 'string' },
|
|
20
|
+
pattern: { type: 'string' },
|
|
21
|
+
maxResults: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }
|
|
22
|
+
},
|
|
23
|
+
required: ['path', 'pattern'],
|
|
24
|
+
additionalProperties: false
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const globFiles = (args = {}, context = {}) => {
|
|
29
|
+
const root = pathPolicy.resolveAllowed(args.path, context);
|
|
30
|
+
const rx = walk.wildcard(args.pattern || '*');
|
|
31
|
+
const max = Math.max(1, Math.min(args.maxResults || 200, 1000));
|
|
32
|
+
const files = walk.walk(root, { filesOnly: true })
|
|
33
|
+
.filter((item) => rx.test(path.basename(item.path)) || rx.test(item.relativePath))
|
|
34
|
+
.slice(0, max);
|
|
35
|
+
return { ok: true, path: root, pattern: args.pattern, files };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const globFilesModule = { descriptor, globFiles };
|
|
39
|
+
Object.assign(exports, globFilesModule);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const zero = require('../../zero-runtime');
|
|
5
|
+
const pathPolicy = require('./path-policy');
|
|
6
|
+
const textModule = require('./text');
|
|
7
|
+
const walk = require('./walk');
|
|
8
|
+
|
|
9
|
+
const descriptor = zero.descriptor.validate({
|
|
10
|
+
name: 'zero.command.filesystem.grepFiles',
|
|
11
|
+
aliases: ['zero.command.grepFiles'],
|
|
12
|
+
description: 'Search text content in files inside allowed roots.',
|
|
13
|
+
capability: 'filesystem.read',
|
|
14
|
+
risk: 'read',
|
|
15
|
+
execution: 'target',
|
|
16
|
+
adapters: ['native'],
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
path: { type: 'string' },
|
|
21
|
+
pattern: { type: 'string' },
|
|
22
|
+
literal: { type: 'boolean', default: false },
|
|
23
|
+
filePattern: { type: 'string' },
|
|
24
|
+
maxResults: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }
|
|
25
|
+
},
|
|
26
|
+
required: ['path', 'pattern'],
|
|
27
|
+
additionalProperties: false
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const grepFiles = (args = {}, context = {}) => {
|
|
32
|
+
const root = pathPolicy.resolveAllowed(args.path, context);
|
|
33
|
+
const max = Math.max(1, Math.min(args.maxResults || 200, 1000));
|
|
34
|
+
const rx = args.literal ? null : new RegExp(String(args.pattern), 'i');
|
|
35
|
+
const fileRx = args.filePattern ? walk.wildcard(args.filePattern) : null;
|
|
36
|
+
const matches = [];
|
|
37
|
+
for (const file of walk.walk(root, { filesOnly: true })) {
|
|
38
|
+
if (fileRx && !fileRx.test(path.basename(file.path)) && !fileRx.test(file.relativePath)) continue;
|
|
39
|
+
let text;
|
|
40
|
+
try { text = textModule.readUtf8(file.path); } catch { continue; }
|
|
41
|
+
const lines = text.split(/\r?\n/);
|
|
42
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
43
|
+
const line = lines[index];
|
|
44
|
+
const hit = args.literal ? line.includes(String(args.pattern)) : rx.test(line);
|
|
45
|
+
if (!hit) continue;
|
|
46
|
+
matches.push({ path: file.path, relativePath: file.relativePath, line: index + 1, text: line });
|
|
47
|
+
if (matches.length >= max) return { ok: true, path: root, pattern: args.pattern, matches };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, path: root, pattern: args.pattern, matches };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const grepFilesModule = { descriptor, grepFiles };
|
|
54
|
+
Object.assign(exports, grepFilesModule);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const zero = require('../../zero-runtime');
|
|
6
|
+
const pathPolicy = require('./path-policy');
|
|
7
|
+
|
|
8
|
+
const descriptor = zero.descriptor.validate({
|
|
9
|
+
name: 'zero.command.filesystem.hashFile',
|
|
10
|
+
aliases: ['zero.command.hashFile'],
|
|
11
|
+
description: 'Calculate SHA-256 hash of a file 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 hashFile = (args = {}, context = {}) => {
|
|
25
|
+
const file = pathPolicy.resolveAllowed(args.path, context);
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
path: file,
|
|
29
|
+
algorithm: 'sha256',
|
|
30
|
+
hash: crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const hashFileModule = { descriptor, hashFile };
|
|
35
|
+
Object.assign(exports, hashFileModule);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const modules = {
|
|
4
|
+
readFile: require('./read-file'),
|
|
5
|
+
readFiles: require('./read-files'),
|
|
6
|
+
listDirectory: require('./list-directory'),
|
|
7
|
+
getFileInfo: require('./get-file-info'),
|
|
8
|
+
globFiles: require('./glob-files'),
|
|
9
|
+
grepFiles: require('./grep-files'),
|
|
10
|
+
writeFile: require('./write-file'),
|
|
11
|
+
appendFile: require('./append-file'),
|
|
12
|
+
createFile: require('./create-file'),
|
|
13
|
+
touchFile: require('./touch-file'),
|
|
14
|
+
replaceFile: require('./replace-file'),
|
|
15
|
+
createDirectory: require('./create-directory'),
|
|
16
|
+
deleteFile: require('./delete-file'),
|
|
17
|
+
copyDirectory: require('./copy-directory'),
|
|
18
|
+
moveDirectory: require('./move-directory'),
|
|
19
|
+
deleteDirectory: require('./delete-directory'),
|
|
20
|
+
truncateFile: require('./truncate-file'),
|
|
21
|
+
copyFile: require('./copy-file'),
|
|
22
|
+
moveFile: require('./move-file'),
|
|
23
|
+
renameFile: require('./rename-file'),
|
|
24
|
+
exists: require('./exists'),
|
|
25
|
+
hashFile: require('./hash-file')
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const filesystem = {};
|
|
29
|
+
for (const [name, item] of Object.entries(modules)) {
|
|
30
|
+
filesystem[name] = item[name];
|
|
31
|
+
filesystem[`${name}Descriptor`] = item.descriptor;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Object.assign(exports, filesystem);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('../../zero-runtime');
|
|
4
|
+
const pathPolicy = require('./path-policy');
|
|
5
|
+
const walk = require('./walk');
|
|
6
|
+
|
|
7
|
+
const descriptor = zero.descriptor.validate({
|
|
8
|
+
name: 'zero.command.filesystem.listDirectory',
|
|
9
|
+
aliases: ['zero.command.listDirectory'],
|
|
10
|
+
description: 'List files and directories recursively inside allowed roots.',
|
|
11
|
+
capability: 'filesystem.read',
|
|
12
|
+
risk: 'read',
|
|
13
|
+
execution: 'target',
|
|
14
|
+
adapters: ['native'],
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
path: { type: 'string' },
|
|
19
|
+
depth: { type: 'integer', minimum: 1, maximum: 5, default: 2 }
|
|
20
|
+
},
|
|
21
|
+
required: ['path'],
|
|
22
|
+
additionalProperties: false
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const listDirectory = (args = {}, context = {}) => {
|
|
27
|
+
const root = pathPolicy.resolveAllowed(args.path, context);
|
|
28
|
+
const depth = Math.max(1, Math.min(args.depth || 2, 5));
|
|
29
|
+
return { ok: true, path: root, depth, entries: walk.walk(root, { depth }) };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const listDirectoryModule = { descriptor, listDirectory };
|
|
33
|
+
Object.assign(exports, listDirectoryModule);
|
|
@@ -0,0 +1,30 @@
|
|
|
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.moveDirectory',
|
|
10
|
+
aliases: ['zero.command.moveDirectory'],
|
|
11
|
+
description: 'Move a directory inside allowed roots.',
|
|
12
|
+
capability: 'filesystem.write',
|
|
13
|
+
risk: 'write',
|
|
14
|
+
execution: 'target',
|
|
15
|
+
adapters: ['native'],
|
|
16
|
+
inputSchema: { type: 'object', properties: { source: { type: 'string' }, destination: { type: 'string' } }, required: ['source', 'destination'], additionalProperties: false }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
20
|
+
const moveDirectory = (args = {}, context = {}) => {
|
|
21
|
+
const source = pathPolicy.resolveAllowed(args.source, context);
|
|
22
|
+
const destination = pathPolicy.resolveAllowed(args.destination, context);
|
|
23
|
+
if (!fs.statSync(source).isDirectory()) throw fail('NOT_A_DIRECTORY', 'source is not a directory');
|
|
24
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
25
|
+
fs.renameSync(source, destination);
|
|
26
|
+
return { ok: true, source, destination };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const moveDirectoryModule = { descriptor, moveDirectory };
|
|
30
|
+
Object.assign(exports, moveDirectoryModule);
|
|
@@ -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.moveFile',
|
|
10
|
+
aliases: ['zero.command.moveFile'],
|
|
11
|
+
description: 'Move one file or directory 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 moveFile = (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.renameSync(source, destination);
|
|
29
|
+
return { ok: true, source, destination };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const moveFileModule = { descriptor, moveFile };
|
|
33
|
+
Object.assign(exports, moveFileModule);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const err = (code, message) => Object.assign(new Error(message), { code });
|
|
7
|
+
const normalize = (value) => path.resolve(String(value)).toLowerCase();
|
|
8
|
+
const rootsFor = (context = {}, env = context.env || process.env) => {
|
|
9
|
+
const explicit = Array.isArray(context.commandRoots) && context.commandRoots.length
|
|
10
|
+
? context.commandRoots
|
|
11
|
+
: (Array.isArray(context.roots) ? context.roots : []);
|
|
12
|
+
const configured = explicit.length ? explicit : String(env.ZERO_AGENT_ROOTS || env.ZERO_COMMAND_ROOTS || '')
|
|
13
|
+
.split(';').map((item) => item.trim()).filter(Boolean);
|
|
14
|
+
const roots = configured.length ? configured : [os.homedir(), process.cwd()];
|
|
15
|
+
return roots.map((item) => path.resolve(String(item)));
|
|
16
|
+
};
|
|
17
|
+
const resolveAllowed = (inputPath, context = {}, env = context.env || process.env) => {
|
|
18
|
+
if (!inputPath || typeof inputPath !== 'string') throw err('INVALID_PATH', 'path is required');
|
|
19
|
+
const roots = rootsFor(context, env);
|
|
20
|
+
const target = path.resolve(inputPath);
|
|
21
|
+
const targetNorm = normalize(target);
|
|
22
|
+
const ok = roots.some((root) => {
|
|
23
|
+
const rootNorm = normalize(root);
|
|
24
|
+
return targetNorm === rootNorm || targetNorm.startsWith(rootNorm + path.sep.toLowerCase());
|
|
25
|
+
});
|
|
26
|
+
if (!ok) throw err('PATH_NOT_ALLOWED', 'path is outside agent allowed roots');
|
|
27
|
+
return target;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const pathPolicy = { rootsFor, resolveAllowed };
|
|
31
|
+
Object.assign(exports, pathPolicy);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('../../zero-runtime');
|
|
4
|
+
const pathPolicy = require('./path-policy');
|
|
5
|
+
const text = require('./text');
|
|
6
|
+
|
|
7
|
+
const descriptor = zero.descriptor.validate({
|
|
8
|
+
name: 'zero.command.filesystem.readFile',
|
|
9
|
+
aliases: ['zero.command.readFile'],
|
|
10
|
+
description: 'Read a UTF-8 text file inside allowed roots with line pagination.',
|
|
11
|
+
capability: 'filesystem.read',
|
|
12
|
+
risk: 'read',
|
|
13
|
+
execution: 'target',
|
|
14
|
+
adapters: ['native'],
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
path: { type: 'string' },
|
|
19
|
+
offset: { type: 'integer', default: 0 },
|
|
20
|
+
length: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }
|
|
21
|
+
},
|
|
22
|
+
required: ['path'],
|
|
23
|
+
additionalProperties: false
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const readFile = (args = {}, context = {}) => {
|
|
27
|
+
const file = pathPolicy.resolveAllowed(args.path, context);
|
|
28
|
+
const lines = text.readUtf8(file).split(/\r?\n/);
|
|
29
|
+
const offset = Number.isInteger(args.offset) ? args.offset : 0;
|
|
30
|
+
const length = Number.isInteger(args.length) ? args.length : 200;
|
|
31
|
+
const start = offset < 0 ? Math.max(lines.length + offset, 0) : Math.max(offset, 0);
|
|
32
|
+
const slice = lines.slice(start, start + length);
|
|
33
|
+
return { ok: true, path: file, totalLines: lines.length, offset: start, length: slice.length, content: text.numbered(slice, start) };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const readFileModule = { descriptor, readFile };
|
|
37
|
+
Object.assign(exports, readFileModule);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('../../zero-runtime');
|
|
4
|
+
const readFileModule = require('./read-file');
|
|
5
|
+
|
|
6
|
+
const descriptor = zero.descriptor.validate({
|
|
7
|
+
name: 'zero.command.filesystem.readFiles',
|
|
8
|
+
aliases: ['zero.command.readFiles'],
|
|
9
|
+
description: 'Read multiple UTF-8 text files inside allowed roots.',
|
|
10
|
+
capability: 'filesystem.read',
|
|
11
|
+
risk: 'read',
|
|
12
|
+
execution: 'target',
|
|
13
|
+
adapters: ['native'],
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
paths: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 50 },
|
|
18
|
+
files: {
|
|
19
|
+
type: 'array', minItems: 1, maxItems: 50,
|
|
20
|
+
items: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
path: { type: 'string' },
|
|
24
|
+
offset: { type: 'integer' },
|
|
25
|
+
length: { type: 'integer', minimum: 1, maximum: 1000 }
|
|
26
|
+
},
|
|
27
|
+
required: ['path'],
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
offset: { type: 'integer', default: 0 },
|
|
32
|
+
length: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
39
|
+
const readFiles = (args = {}, context = {}) => {
|
|
40
|
+
const requests = Array.isArray(args.files) && args.files.length
|
|
41
|
+
? args.files.map((item) => ({ path: item.path, offset: item.offset, length: item.length }))
|
|
42
|
+
: (Array.isArray(args.paths) ? args.paths : []).map((path) => ({ path, offset: args.offset, length: args.length }));
|
|
43
|
+
if (!requests.length) throw fail('INVALID_PATHS', 'paths or files must contain at least one file');
|
|
44
|
+
if (requests.length > 50) throw fail('TOO_MANY_PATHS', 'readFiles supports up to 50 files');
|
|
45
|
+
const files = requests.map((request) => {
|
|
46
|
+
try { return readFileModule.readFile(request, context); }
|
|
47
|
+
catch (error) {
|
|
48
|
+
return { ok: false, path: String(request.path), error: { code: error.code || 'READ_FAILED', message: error.message || String(error) } };
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const succeeded = files.filter((item) => item.ok).length;
|
|
52
|
+
return { ok: succeeded === files.length, count: files.length, succeeded, failed: files.length - succeeded, files };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const readFilesModule = { descriptor, readFiles };
|
|
56
|
+
Object.assign(exports, readFilesModule);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('../../zero-runtime');
|
|
4
|
+
const moveFileModule = require('./move-file');
|
|
5
|
+
|
|
6
|
+
const descriptor = zero.descriptor.validate({
|
|
7
|
+
name: 'zero.command.filesystem.renameFile',
|
|
8
|
+
aliases: ['zero.command.renameFile'],
|
|
9
|
+
description: 'Rename one file or directory inside allowed roots.',
|
|
10
|
+
capability: 'filesystem.write',
|
|
11
|
+
risk: 'write',
|
|
12
|
+
execution: 'target',
|
|
13
|
+
adapters: ['native'],
|
|
14
|
+
inputSchema: moveFileModule.descriptor.inputSchema
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const renameFile = (args = {}, context = {}) => moveFileModule.moveFile(args, context);
|
|
18
|
+
|
|
19
|
+
const renameFileModule = { descriptor, renameFile };
|
|
20
|
+
Object.assign(exports, renameFileModule);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const zero = require('../../zero-runtime');
|
|
5
|
+
const pathPolicy = require('./path-policy');
|
|
6
|
+
const textModule = require('./text');
|
|
7
|
+
|
|
8
|
+
const descriptor = zero.descriptor.validate({
|
|
9
|
+
name: 'zero.command.filesystem.replaceFile',
|
|
10
|
+
aliases: ['zero.command.replaceFile'],
|
|
11
|
+
description: 'Replace exact text inside a UTF-8 file in 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
|
+
oldString: { type: 'string' },
|
|
21
|
+
newString: { type: 'string' },
|
|
22
|
+
expectedReplacements: { type: 'integer', minimum: 0 }
|
|
23
|
+
},
|
|
24
|
+
required: ['path', 'oldString', 'newString'],
|
|
25
|
+
additionalProperties: false
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
30
|
+
const replaceFile = (args = {}, context = {}) => {
|
|
31
|
+
const file = pathPolicy.resolveAllowed(args.path, context);
|
|
32
|
+
const oldString = String(args.oldString ?? '');
|
|
33
|
+
if (!oldString) throw fail('INVALID_REPLACE', 'oldString must not be empty');
|
|
34
|
+
const newString = String(args.newString ?? '');
|
|
35
|
+
const text = textModule.readUtf8(file);
|
|
36
|
+
const replacements = text.split(oldString).length - 1;
|
|
37
|
+
if (Number.isInteger(args.expectedReplacements) && replacements !== args.expectedReplacements) {
|
|
38
|
+
throw fail('REPLACE_COUNT_MISMATCH', `expected ${args.expectedReplacements} replacements, got ${replacements}`);
|
|
39
|
+
}
|
|
40
|
+
fs.writeFileSync(file, text.split(oldString).join(newString), 'utf8');
|
|
41
|
+
return { ok: true, path: file, replacements };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const replaceFileModule = { descriptor, replaceFile };
|
|
45
|
+
Object.assign(exports, replaceFileModule);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
|
|
5
|
+
const textDecoder = new TextDecoder('utf-8', { fatal: false });
|
|
6
|
+
const err = (code, message) => Object.assign(new Error(message), { code });
|
|
7
|
+
const readUtf8 = (file) => {
|
|
8
|
+
const data = fs.readFileSync(file);
|
|
9
|
+
if (data.includes(0)) throw err('BINARY_FILE', 'binary file is not supported');
|
|
10
|
+
return textDecoder.decode(data);
|
|
11
|
+
};
|
|
12
|
+
const numbered = (lines, startIndex) => lines
|
|
13
|
+
.map((line, index) => String(startIndex + index + 1).padStart(4, ' ') + '\t' + line)
|
|
14
|
+
.join('\n');
|
|
15
|
+
|
|
16
|
+
const text = { readUtf8, numbered };
|
|
17
|
+
Object.assign(exports, text);
|
|
@@ -0,0 +1,29 @@
|
|
|
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.touchFile',
|
|
10
|
+
aliases: ['zero.command.touchFile'],
|
|
11
|
+
description: 'Create or update a file timestamp inside allowed roots.',
|
|
12
|
+
capability: 'filesystem.write',
|
|
13
|
+
risk: 'write',
|
|
14
|
+
execution: 'target',
|
|
15
|
+
adapters: ['native'],
|
|
16
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'], additionalProperties: false }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const touchFile = (args = {}, context = {}) => {
|
|
20
|
+
const file = pathPolicy.resolveAllowed(args.path, context);
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
if (!fs.existsSync(file)) fs.closeSync(fs.openSync(file, 'w'));
|
|
23
|
+
const now = new Date();
|
|
24
|
+
fs.utimesSync(file, now, now);
|
|
25
|
+
return { ok: true, path: file, touched: true };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const touchFileModule = { descriptor, touchFile };
|
|
29
|
+
Object.assign(exports, touchFileModule);
|
|
@@ -0,0 +1,26 @@
|
|
|
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.truncateFile',
|
|
9
|
+
aliases: ['zero.command.truncateFile'],
|
|
10
|
+
description: 'Truncate a file to a specific byte size inside allowed roots.',
|
|
11
|
+
capability: 'filesystem.write',
|
|
12
|
+
risk: 'write',
|
|
13
|
+
execution: 'target',
|
|
14
|
+
adapters: ['native'],
|
|
15
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' }, size: { type: 'integer', minimum: 0, default: 0 } }, required: ['path'], additionalProperties: false }
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const truncateFile = (args = {}, context = {}) => {
|
|
19
|
+
const file = pathPolicy.resolveAllowed(args.path, context);
|
|
20
|
+
const size = Number.isInteger(args.size) && args.size >= 0 ? args.size : 0;
|
|
21
|
+
fs.truncateSync(file, size);
|
|
22
|
+
return { ok: true, path: file, size };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const truncateFileModule = { descriptor, truncateFile };
|
|
26
|
+
Object.assign(exports, truncateFileModule);
|
|
@@ -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);
|