@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.
Files changed (50) hide show
  1. package/README.md +337 -0
  2. package/package.json +32 -0
  3. package/src/audit/logger.js +30 -0
  4. package/src/cli/commands/cloud.js +89 -0
  5. package/src/cli/commands/doctor.js +18 -0
  6. package/src/cli/commands/login.js +59 -0
  7. package/src/cli/commands/logout.js +20 -0
  8. package/src/cli/commands/remote.js +64 -0
  9. package/src/cli/commands/status.js +32 -0
  10. package/src/cli/commands/update.js +31 -0
  11. package/src/cli/help.js +52 -0
  12. package/src/cli/main.js +55 -0
  13. package/src/cli/options.js +34 -0
  14. package/src/cli.js +20 -0
  15. package/src/core/dispatcher.js +49 -0
  16. package/src/core/errors.js +37 -0
  17. package/src/core/permissions.js +53 -0
  18. package/src/core/runtime.js +45 -0
  19. package/src/events/bus.js +39 -0
  20. package/src/permissions/approvals.js +47 -0
  21. package/src/permissions/controller.js +92 -0
  22. package/src/permissions/evaluator.js +93 -0
  23. package/src/permissions/manager.js +57 -0
  24. package/src/permissions/policy.js +28 -0
  25. package/src/permissions/store.js +27 -0
  26. package/src/security/path.js +61 -0
  27. package/src/security/scope.js +45 -0
  28. package/src/server.js +1 -0
  29. package/src/services/commands.js +107 -0
  30. package/src/services/files.js +104 -0
  31. package/src/services/index.js +11 -0
  32. package/src/services/system.js +17 -0
  33. package/src/transport/cloud.js +206 -0
  34. package/src/transport/mcp/manifest-store.js +32 -0
  35. package/src/transport/mcp/response.js +25 -0
  36. package/src/transport/mcp/server.js +114 -0
  37. package/src/transport/mcp/tools/commands.js +33 -0
  38. package/src/transport/mcp/tools/files.js +55 -0
  39. package/src/transport/mcp/tools/index.js +1 -0
  40. package/src/transport/mcp/tools/registry.js +92 -0
  41. package/src/transport/mcp/tools/system.js +16 -0
  42. package/src/tui/app.js +392 -0
  43. package/src/tui/commands.js +71 -0
  44. package/src/tui/index.js +37 -0
  45. package/src/tui/layout.js +48 -0
  46. package/src/tui/model.js +62 -0
  47. package/src/tui/profiles.js +32 -0
  48. package/src/utils/credentials.js +35 -0
  49. package/src/utils/text.js +180 -0
  50. 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
+ }