@buildifyx/desktop-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +337 -0
- package/package.json +32 -0
- package/src/audit/logger.js +30 -0
- package/src/cli/commands/cloud.js +89 -0
- package/src/cli/commands/doctor.js +18 -0
- package/src/cli/commands/login.js +59 -0
- package/src/cli/commands/logout.js +20 -0
- package/src/cli/commands/remote.js +64 -0
- package/src/cli/commands/status.js +32 -0
- package/src/cli/commands/update.js +31 -0
- package/src/cli/help.js +52 -0
- package/src/cli/main.js +55 -0
- package/src/cli/options.js +34 -0
- package/src/cli.js +20 -0
- package/src/core/dispatcher.js +49 -0
- package/src/core/errors.js +37 -0
- package/src/core/permissions.js +53 -0
- package/src/core/runtime.js +45 -0
- package/src/events/bus.js +39 -0
- package/src/permissions/approvals.js +47 -0
- package/src/permissions/controller.js +92 -0
- package/src/permissions/evaluator.js +93 -0
- package/src/permissions/manager.js +57 -0
- package/src/permissions/policy.js +28 -0
- package/src/permissions/store.js +27 -0
- package/src/security/path.js +61 -0
- package/src/security/scope.js +45 -0
- package/src/server.js +1 -0
- package/src/services/commands.js +107 -0
- package/src/services/files.js +104 -0
- package/src/services/index.js +11 -0
- package/src/services/system.js +17 -0
- package/src/transport/cloud.js +206 -0
- package/src/transport/mcp/manifest-store.js +32 -0
- package/src/transport/mcp/response.js +25 -0
- package/src/transport/mcp/server.js +114 -0
- package/src/transport/mcp/tools/commands.js +33 -0
- package/src/transport/mcp/tools/files.js +55 -0
- package/src/transport/mcp/tools/index.js +1 -0
- package/src/transport/mcp/tools/registry.js +92 -0
- package/src/transport/mcp/tools/system.js +16 -0
- package/src/tui/app.js +392 -0
- package/src/tui/commands.js +71 -0
- package/src/tui/index.js +37 -0
- package/src/tui/layout.js +48 -0
- package/src/tui/model.js +62 -0
- package/src/tui/profiles.js +32 -0
- package/src/utils/credentials.js +35 -0
- package/src/utils/text.js +180 -0
- package/src/version.js +111 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export function defaultToolManifestPath() {
|
|
6
|
+
return path.join(os.homedir(), '.buildifyx', 'tool-manifest.json');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function loadPreviousToolManifest(filePath = defaultToolManifestPath()) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function saveToolManifest(manifest, filePath = defaultToolManifestPath()) {
|
|
18
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
19
|
+
await writeFile(filePath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
20
|
+
return filePath;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function inspectToolManifest(manifest, filePath = defaultToolManifestPath()) {
|
|
24
|
+
const previous = await loadPreviousToolManifest(filePath);
|
|
25
|
+
const changed = Boolean(previous?.hash && previous.hash !== manifest.hash);
|
|
26
|
+
await saveToolManifest(manifest, filePath);
|
|
27
|
+
return {
|
|
28
|
+
previousHash: previous?.hash ?? null,
|
|
29
|
+
changed,
|
|
30
|
+
filePath
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { normalizeError } from '../../core/errors.js';
|
|
2
|
+
|
|
3
|
+
export function successResult(structuredContent, { isError } = {}) {
|
|
4
|
+
return {
|
|
5
|
+
content: [{ type: 'text', text: JSON.stringify(structuredContent, null, 2) }],
|
|
6
|
+
structuredContent,
|
|
7
|
+
...(isError ? { isError: true } : {})
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function errorResult(error) {
|
|
12
|
+
const normalized = normalizeError(error);
|
|
13
|
+
const structuredContent = {
|
|
14
|
+
error: {
|
|
15
|
+
code: normalized.code,
|
|
16
|
+
message: normalized.message,
|
|
17
|
+
...(normalized.details !== undefined ? { details: normalized.details } : {})
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: 'text', text: JSON.stringify(structuredContent, null, 2) }],
|
|
22
|
+
structuredContent,
|
|
23
|
+
isError: true
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { toNodeHandler } from '@modelcontextprotocol/node';
|
|
4
|
+
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
|
|
5
|
+
import { createRuntime } from '../../core/runtime.js';
|
|
6
|
+
import { createToolManifest, registerMcpToolRegistry } from './tools/index.js';
|
|
7
|
+
import { inspectToolManifest } from './manifest-store.js';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { version: packageVersion } = require('../../../package.json');
|
|
11
|
+
|
|
12
|
+
export function isAllowedOrigin(origin) {
|
|
13
|
+
if (!origin) return true;
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(origin);
|
|
16
|
+
return url.hostname === '127.0.0.1' || url.hostname === 'localhost' || url.hostname === '::1';
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildMcpServer({ runtime, root, fullAccess = false, policy, manifest }) {
|
|
23
|
+
const activeRuntime = runtime ?? createRuntime({ root, fullAccess, policy });
|
|
24
|
+
const activeManifest = manifest ?? createToolManifest({ fullAccess: activeRuntime.fullAccess });
|
|
25
|
+
const server = new McpServer({ name: 'buildifyx-desktop-agent', version: packageVersion });
|
|
26
|
+
registerMcpToolRegistry(server, activeRuntime.dispatch, { fullAccess: activeRuntime.fullAccess });
|
|
27
|
+
return { server, runtime: activeRuntime, manifest: activeManifest };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function startMcpServer({ root, port, fullAccess = false, policy, runtime, quiet = false }) {
|
|
31
|
+
const activeRuntime = runtime ?? createRuntime({ root, fullAccess, policy });
|
|
32
|
+
const manifest = createToolManifest({ fullAccess: activeRuntime.fullAccess });
|
|
33
|
+
const manifestState = await inspectToolManifest(manifest);
|
|
34
|
+
|
|
35
|
+
const handler = createMcpHandler(() => buildMcpServer({ runtime: activeRuntime, manifest }).server, {
|
|
36
|
+
responseMode: 'json'
|
|
37
|
+
});
|
|
38
|
+
const nodeHandler = toNodeHandler(handler);
|
|
39
|
+
|
|
40
|
+
const httpServer = createServer((req, res) => {
|
|
41
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
42
|
+
|
|
43
|
+
if (url.pathname === '/health' && req.method === 'GET') {
|
|
44
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
45
|
+
res.end(JSON.stringify({
|
|
46
|
+
ok: true,
|
|
47
|
+
version: packageVersion,
|
|
48
|
+
mode: activeRuntime.fullAccess ? 'full_access' : 'restricted',
|
|
49
|
+
tools: {
|
|
50
|
+
count: manifest.count,
|
|
51
|
+
hash: manifest.hash,
|
|
52
|
+
shortHash: manifest.shortHash,
|
|
53
|
+
changedSinceLastRun: manifestState.changed
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (url.pathname === '/tools' && req.method === 'GET') {
|
|
60
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
61
|
+
res.end(JSON.stringify({
|
|
62
|
+
version: packageVersion,
|
|
63
|
+
changedSinceLastRun: manifestState.changed,
|
|
64
|
+
previousHash: manifestState.previousHash,
|
|
65
|
+
...manifest
|
|
66
|
+
}, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (url.pathname !== '/mcp') {
|
|
71
|
+
res.writeHead(404, { 'content-type': 'application/json' });
|
|
72
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!isAllowedOrigin(req.headers.origin)) {
|
|
76
|
+
res.writeHead(403, { 'content-type': 'application/json' });
|
|
77
|
+
res.end(JSON.stringify({ error: 'Origin not allowed' }));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
void nodeHandler(req, res);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await new Promise((resolve, reject) => {
|
|
84
|
+
httpServer.once('error', reject);
|
|
85
|
+
httpServer.listen(port, '127.0.0.1', resolve);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!quiet) {
|
|
89
|
+
console.log('Buildifyx Desktop Agent');
|
|
90
|
+
console.log(`Version: ${packageVersion}`);
|
|
91
|
+
console.log(`Root: ${root}`);
|
|
92
|
+
console.log(`MCP: http://127.0.0.1:${port}/mcp`);
|
|
93
|
+
console.log(`Health: http://127.0.0.1:${port}/health`);
|
|
94
|
+
console.log(`Tools: ${manifest.count} (${manifest.shortHash})${manifestState.changed ? ' CHANGED' : ''}`);
|
|
95
|
+
console.log(`Mode: ${activeRuntime.fullAccess ? 'FULL ACCESS (not sandboxed)' : 'restricted'}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function close() {
|
|
99
|
+
await handler.close();
|
|
100
|
+
await new Promise((resolve) => httpServer.close(resolve));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
close,
|
|
105
|
+
httpServer,
|
|
106
|
+
runtime: activeRuntime,
|
|
107
|
+
version: packageVersion,
|
|
108
|
+
manifest,
|
|
109
|
+
manifestState,
|
|
110
|
+
mcpUrl: `http://127.0.0.1:${port}/mcp`,
|
|
111
|
+
healthUrl: `http://127.0.0.1:${port}/health`,
|
|
112
|
+
toolsUrl: `http://127.0.0.1:${port}/tools`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { MAX_OUTPUT_BYTES, MAX_TIMEOUT_MS } from '../../../services/commands.js';
|
|
3
|
+
import { successResult } from '../response.js';
|
|
4
|
+
|
|
5
|
+
const RunCommandInput = z.object({
|
|
6
|
+
command: z.string().min(1).describe('Allowed executable name. Paths and shell command strings are not accepted.'),
|
|
7
|
+
args: z.array(z.string().max(4096)).max(128).default([]),
|
|
8
|
+
cwd: z.string().default('.').describe('Working directory relative to the configured root directory.'),
|
|
9
|
+
timeoutMs: z.number().int().min(100).max(MAX_TIMEOUT_MS).default(15_000)
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function getCommandToolDefinitions({ fullAccess = false } = {}) {
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
name: 'run_command',
|
|
16
|
+
title: 'Run command',
|
|
17
|
+
description: fullAccess
|
|
18
|
+
? `Run a command in FULL ACCESS mode. Commands are not sandboxed. Timeout: ${MAX_TIMEOUT_MS} ms; output: ${MAX_OUTPUT_BYTES} bytes per stream.`
|
|
19
|
+
: `Run a restricted developer command without a shell. cwd must stay inside an allowed root unless approved. Timeout: ${MAX_TIMEOUT_MS} ms; output: ${MAX_OUTPUT_BYTES} bytes per stream.`,
|
|
20
|
+
permission: 'command',
|
|
21
|
+
inputSchema: RunCommandInput,
|
|
22
|
+
annotations: {
|
|
23
|
+
readOnlyHint: false,
|
|
24
|
+
destructiveHint: true,
|
|
25
|
+
idempotentHint: false,
|
|
26
|
+
openWorldHint: true
|
|
27
|
+
},
|
|
28
|
+
toMcpResult(result) {
|
|
29
|
+
return successResult(result, { isError: result.exitCode !== 0 || result.timedOut });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { MAX_TEXT_FILE_BYTES } from '../../../services/files.js';
|
|
3
|
+
|
|
4
|
+
const FilePath = z.string().min(1).describe('Path relative to the configured root directory.');
|
|
5
|
+
|
|
6
|
+
const schemas = {
|
|
7
|
+
list_directory: z.object({
|
|
8
|
+
path: z.string().default('.').describe('Path relative to the configured root directory.'),
|
|
9
|
+
limit: z.number().int().min(1).max(500).default(200)
|
|
10
|
+
}),
|
|
11
|
+
read_file: z.object({ path: FilePath }),
|
|
12
|
+
write_file: z.object({ path: FilePath, content: z.string() }),
|
|
13
|
+
edit_file: z.discriminatedUnion('operation', [
|
|
14
|
+
z.object({ path: FilePath, operation: z.literal('overwrite'), content: z.string() }),
|
|
15
|
+
z.object({ path: FilePath, operation: z.literal('replace_lines'), startLine: z.number().int().min(1), endLine: z.number().int().min(1), lines: z.array(z.string()) }),
|
|
16
|
+
z.object({ path: FilePath, operation: z.literal('replace_characters'), start: z.object({ line: z.number().int().min(1), column: z.number().int().min(1) }), end: z.object({ line: z.number().int().min(1), column: z.number().int().min(1) }), content: z.string() })
|
|
17
|
+
])
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function getFileToolDefinitions() {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
name: 'list_directory',
|
|
24
|
+
title: 'List directory',
|
|
25
|
+
description: 'List files and folders inside the configured root directory.',
|
|
26
|
+
permission: 'read',
|
|
27
|
+
inputSchema: schemas.list_directory,
|
|
28
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'read_file',
|
|
32
|
+
title: 'Read file',
|
|
33
|
+
description: `Read one UTF-8 text file inside the configured root. Files are limited to ${MAX_TEXT_FILE_BYTES} bytes.`,
|
|
34
|
+
permission: 'read',
|
|
35
|
+
inputSchema: schemas.read_file,
|
|
36
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'write_file',
|
|
40
|
+
title: 'Write file',
|
|
41
|
+
description: 'Create one new UTF-8 text file inside the configured root. Existing files are never overwritten.',
|
|
42
|
+
permission: 'write',
|
|
43
|
+
inputSchema: schemas.write_file,
|
|
44
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'edit_file',
|
|
48
|
+
title: 'Edit file',
|
|
49
|
+
description: 'Edit an existing UTF-8 text file inside the configured root.',
|
|
50
|
+
permission: 'write',
|
|
51
|
+
inputSchema: schemas.edit_file,
|
|
52
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createToolManifest, getMcpToolDefinitions, notifyToolListChanged, registerMcpToolRegistry } from './registry.js';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import * as z from 'zod/v4';
|
|
3
|
+
import { getCommandToolDefinitions } from './commands.js';
|
|
4
|
+
import { getFileToolDefinitions } from './files.js';
|
|
5
|
+
import { getSystemToolDefinitions } from './system.js';
|
|
6
|
+
import { errorResult, successResult } from '../response.js';
|
|
7
|
+
|
|
8
|
+
const MANIFEST_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
function stableValue(value) {
|
|
11
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
12
|
+
if (value && typeof value === 'object') {
|
|
13
|
+
return Object.fromEntries(
|
|
14
|
+
Object.keys(value)
|
|
15
|
+
.sort()
|
|
16
|
+
.map((key) => [key, stableValue(value[key])])
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function jsonSchema(inputSchema) {
|
|
23
|
+
return inputSchema ? z.toJSONSchema(inputSchema) : { type: 'object', properties: {}, additionalProperties: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getMcpToolDefinitions(options = {}) {
|
|
27
|
+
return [
|
|
28
|
+
...getSystemToolDefinitions(options),
|
|
29
|
+
...getFileToolDefinitions(options),
|
|
30
|
+
...getCommandToolDefinitions(options)
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createToolManifest(options = {}) {
|
|
35
|
+
const tools = getMcpToolDefinitions(options)
|
|
36
|
+
.map((definition) => ({
|
|
37
|
+
name: definition.name,
|
|
38
|
+
title: definition.title,
|
|
39
|
+
description: definition.description,
|
|
40
|
+
permission: definition.permission,
|
|
41
|
+
annotations: stableValue(definition.annotations ?? {}),
|
|
42
|
+
inputSchema: stableValue(jsonSchema(definition.inputSchema))
|
|
43
|
+
}))
|
|
44
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
45
|
+
|
|
46
|
+
const fingerprintSource = stableValue({ manifestVersion: MANIFEST_VERSION, tools });
|
|
47
|
+
const hash = createHash('sha256').update(JSON.stringify(fingerprintSource)).digest('hex');
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
manifestVersion: MANIFEST_VERSION,
|
|
51
|
+
count: tools.length,
|
|
52
|
+
hash,
|
|
53
|
+
shortHash: hash.slice(0, 8),
|
|
54
|
+
tools
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function registerMcpToolRegistry(server, dispatch, options = {}) {
|
|
59
|
+
const definitions = getMcpToolDefinitions(options);
|
|
60
|
+
|
|
61
|
+
for (const definition of definitions) {
|
|
62
|
+
const config = {
|
|
63
|
+
title: definition.title,
|
|
64
|
+
description: definition.description,
|
|
65
|
+
annotations: definition.annotations
|
|
66
|
+
};
|
|
67
|
+
if (definition.inputSchema) config.inputSchema = definition.inputSchema;
|
|
68
|
+
|
|
69
|
+
server.registerTool(definition.name, config, async (input = {}) => {
|
|
70
|
+
try {
|
|
71
|
+
const result = await dispatch(definition.name, input ?? {});
|
|
72
|
+
return definition.toMcpResult
|
|
73
|
+
? definition.toMcpResult(result)
|
|
74
|
+
: successResult(result);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return errorResult(error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return definitions;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function notifyToolListChanged(server) {
|
|
85
|
+
if (!server || typeof server.sendToolListChanged !== 'function') return false;
|
|
86
|
+
try {
|
|
87
|
+
server.sendToolListChanged();
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function getSystemToolDefinitions() {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
name: 'get_system_info',
|
|
5
|
+
title: 'Get system info',
|
|
6
|
+
description: 'Return basic read-only information about the local machine running the Buildifyx agent.',
|
|
7
|
+
permission: 'read',
|
|
8
|
+
annotations: {
|
|
9
|
+
readOnlyHint: true,
|
|
10
|
+
destructiveHint: false,
|
|
11
|
+
idempotentHint: true,
|
|
12
|
+
openWorldHint: false
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
];
|
|
16
|
+
}
|