@livedesk/hub 0.1.40 → 0.1.42

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.
@@ -1,100 +1,100 @@
1
- import readline from 'node:readline';
2
- import { AGENT_TOOL_DEFINITIONS } from './agent-tool-registry.js';
3
-
4
- const hubUrl = String(process.env.LIVEDESK_AGENT_MCP_URL || '').replace(/\/+$/, '');
5
- const token = String(process.env.LIVEDESK_AGENT_MCP_TOKEN || '');
6
- const protocolVersion = '2025-06-18';
7
-
8
- const toolDefinitions = AGENT_TOOL_DEFINITIONS.map(tool => ({
9
- name: tool.name,
10
- description: tool.description,
11
- inputSchema: { $schema: 'https://json-schema.org/draft/2020-12/schema', ...tool.inputSchema },
12
- annotations: {
13
- readOnlyHint: tool.readOnly === true,
14
- // Codex exec cannot service an interactive MCP approval prompt. Device
15
- // mutations are proposals at this boundary and remain fail-closed behind
16
- // LiveDesk's own signed permission policy and Hub approval workflow.
17
- destructiveHint: false,
18
- idempotentHint: tool.readOnly === true || tool.reversible === true,
19
- openWorldHint: false
20
- }
21
- }));
22
-
23
- function response(id, result) {
24
- process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
25
- }
26
-
27
- function errorResponse(id, code, message) {
28
- process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } })}\n`);
29
- }
30
-
31
- async function callHub(name, args) {
32
- if (!hubUrl || !token) throw new Error('LiveDesk MCP session is not configured.');
33
- const result = await fetch(`${hubUrl}/api/internal/agent-mcp/tool`, {
34
- method: 'POST',
35
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
36
- body: JSON.stringify({ name, arguments: args && typeof args === 'object' ? args : {} })
37
- });
38
- const body = await result.json().catch(() => ({}));
39
- if (!result.ok || body.ok !== true) throw new Error(String(body.error || `LiveDesk tool failed (${result.status}).`));
40
- return body.result;
41
- }
42
-
43
- async function handle(message) {
44
- const id = message?.id;
45
- const method = String(message?.method || '');
46
- if (!method) return;
47
- if (method === 'initialize') {
48
- response(id, {
49
- protocolVersion,
50
- capabilities: { tools: { listChanged: false } },
51
- serverInfo: { name: 'livedesk', version: '1.0.0' }
52
- });
53
- return;
54
- }
55
- if (method === 'notifications/initialized' || method === 'ping') {
56
- if (id !== undefined) response(id, {});
57
- return;
58
- }
59
- if (method === 'tools/list') {
60
- response(id, { tools: toolDefinitions });
61
- return;
62
- }
63
- if (method === 'tools/call') {
64
- const name = String(message?.params?.name || '');
65
- if (!toolDefinitions.some(tool => tool.name === name)) {
66
- errorResponse(id, -32602, 'Unknown LiveDesk tool.');
67
- return;
68
- }
69
- try {
70
- const result = await callHub(name, message?.params?.arguments || {});
71
- response(id, {
72
- content: [{ type: 'text', text: JSON.stringify(result) }],
73
- structuredContent: result,
74
- isError: false
75
- });
76
- } catch (error) {
77
- response(id, {
78
- content: [{ type: 'text', text: String(error?.message || 'LiveDesk tool failed.') }],
79
- isError: true
80
- });
81
- }
82
- return;
83
- }
84
- if (id !== undefined) errorResponse(id, -32601, `Unsupported MCP method: ${method}`);
85
- }
86
-
87
- const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
88
- input.on('line', line => {
89
- const trimmed = String(line || '').trim();
90
- if (!trimmed) return;
91
- let message;
92
- try {
93
- message = JSON.parse(trimmed);
94
- } catch {
95
- return;
96
- }
97
- void handle(message).catch(error => {
98
- if (message?.id !== undefined) errorResponse(message.id, -32603, String(error?.message || 'MCP server error.'));
99
- });
100
- });
1
+ import readline from 'node:readline';
2
+ import { AGENT_TOOL_DEFINITIONS } from './agent-tool-registry.js';
3
+
4
+ const hubUrl = String(process.env.LIVEDESK_AGENT_MCP_URL || '').replace(/\/+$/, '');
5
+ const token = String(process.env.LIVEDESK_AGENT_MCP_TOKEN || '');
6
+ const protocolVersion = '2025-06-18';
7
+
8
+ const toolDefinitions = AGENT_TOOL_DEFINITIONS.map(tool => ({
9
+ name: tool.name,
10
+ description: tool.description,
11
+ inputSchema: { $schema: 'https://json-schema.org/draft/2020-12/schema', ...tool.inputSchema },
12
+ annotations: {
13
+ readOnlyHint: tool.readOnly === true,
14
+ // Codex exec cannot service an interactive MCP approval prompt. Device
15
+ // mutations are proposals at this boundary and remain fail-closed behind
16
+ // LiveDesk's own signed permission policy and Hub approval workflow.
17
+ destructiveHint: false,
18
+ idempotentHint: tool.readOnly === true || tool.reversible === true,
19
+ openWorldHint: false
20
+ }
21
+ }));
22
+
23
+ function response(id, result) {
24
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
25
+ }
26
+
27
+ function errorResponse(id, code, message) {
28
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } })}\n`);
29
+ }
30
+
31
+ async function callHub(name, args) {
32
+ if (!hubUrl || !token) throw new Error('LiveDesk MCP session is not configured.');
33
+ const result = await fetch(`${hubUrl}/api/internal/agent-mcp/tool`, {
34
+ method: 'POST',
35
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ name, arguments: args && typeof args === 'object' ? args : {} })
37
+ });
38
+ const body = await result.json().catch(() => ({}));
39
+ if (!result.ok || body.ok !== true) throw new Error(String(body.error || `LiveDesk tool failed (${result.status}).`));
40
+ return body.result;
41
+ }
42
+
43
+ async function handle(message) {
44
+ const id = message?.id;
45
+ const method = String(message?.method || '');
46
+ if (!method) return;
47
+ if (method === 'initialize') {
48
+ response(id, {
49
+ protocolVersion,
50
+ capabilities: { tools: { listChanged: false } },
51
+ serverInfo: { name: 'livedesk', version: '1.0.0' }
52
+ });
53
+ return;
54
+ }
55
+ if (method === 'notifications/initialized' || method === 'ping') {
56
+ if (id !== undefined) response(id, {});
57
+ return;
58
+ }
59
+ if (method === 'tools/list') {
60
+ response(id, { tools: toolDefinitions });
61
+ return;
62
+ }
63
+ if (method === 'tools/call') {
64
+ const name = String(message?.params?.name || '');
65
+ if (!toolDefinitions.some(tool => tool.name === name)) {
66
+ errorResponse(id, -32602, 'Unknown LiveDesk tool.');
67
+ return;
68
+ }
69
+ try {
70
+ const result = await callHub(name, message?.params?.arguments || {});
71
+ response(id, {
72
+ content: [{ type: 'text', text: JSON.stringify(result) }],
73
+ structuredContent: result,
74
+ isError: false
75
+ });
76
+ } catch (error) {
77
+ response(id, {
78
+ content: [{ type: 'text', text: String(error?.message || 'LiveDesk tool failed.') }],
79
+ isError: true
80
+ });
81
+ }
82
+ return;
83
+ }
84
+ if (id !== undefined) errorResponse(id, -32601, `Unsupported MCP method: ${method}`);
85
+ }
86
+
87
+ const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
88
+ input.on('line', line => {
89
+ const trimmed = String(line || '').trim();
90
+ if (!trimmed) return;
91
+ let message;
92
+ try {
93
+ message = JSON.parse(trimmed);
94
+ } catch {
95
+ return;
96
+ }
97
+ void handle(message).catch(error => {
98
+ if (message?.id !== undefined) errorResponse(message.id, -32603, String(error?.message || 'MCP server error.'));
99
+ });
100
+ });
@@ -1,187 +1,187 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
4
- import { isPathWithinRoot } from './path-registry.js';
5
-
6
- function filesystemError(message, code = 'FILESYSTEM_ERROR', status = 400) {
7
- const error = new Error(message);
8
- error.code = code;
9
- error.status = status;
10
- return error;
11
- }
12
-
13
- function safeEntryName(name) {
14
- return String(name || '').replace(/[\0\r\n\t]/g, ' ').slice(0, 512);
15
- }
16
-
17
- function toPublicEntry(entry) {
18
- return {
19
- id: entry.id,
20
- parentId: entry.parentId,
21
- name: entry.name,
22
- type: entry.type,
23
- size: entry.size || 0,
24
- modifiedAt: entry.modifiedAt || null,
25
- extension: entry.type === 'file' ? path.extname(entry.name).toLowerCase() : '',
26
- hasChildren: entry.type !== 'file' && entry.hasChildren === true,
27
- displayPath: entry.displayPath,
28
- locked: entry.locked === true
29
- };
30
- }
31
-
32
- async function readEntryStat(absolutePath) {
33
- try {
34
- return await fs.lstat(absolutePath);
35
- } catch (error) {
36
- if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
37
- if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') throw filesystemError('filesystem-entry-unavailable', 'NOT_FOUND', 404);
38
- throw error;
39
- }
40
- }
41
-
42
- export async function readDirectoryEntries(registry, folderId) {
43
- const folder = registry.resolve(folderId);
44
- if (folder.type === 'file') throw filesystemError('filesystem-entry-is-not-folder', 'NOT_A_FOLDER', 400);
45
- if (folder.locked) throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
46
- let entries;
47
- try {
48
- entries = await fs.readdir(folder.absolutePath, { withFileTypes: true });
49
- } catch (error) {
50
- if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
51
- if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') throw filesystemError('filesystem-entry-unavailable', 'NOT_FOUND', 404);
52
- throw error;
53
- }
54
-
55
- const output = [];
56
- for (const dirent of entries) {
57
- const name = safeEntryName(dirent.name);
58
- if (!name || name === '.' || name === '..') continue;
59
- const absolutePath = path.resolve(folder.absolutePath, name);
60
- if (!isPathWithinRoot(folder.rootPath, absolutePath)) continue;
61
- const stat = await readEntryStat(absolutePath).catch(error => {
62
- if (error?.code === 'ACCESS_DENIED' || error?.code === 'NOT_FOUND') return null;
63
- throw error;
64
- });
65
- if (!stat) continue;
66
- const isSymlink = dirent.isSymbolicLink() || stat.isSymbolicLink();
67
- const type = !isSymlink && (dirent.isDirectory() || stat.isDirectory()) ? 'folder' : 'file';
68
- const child = registry.register({
69
- absolutePath,
70
- rootPath: folder.rootPath,
71
- rootId: folder.rootId,
72
- parentId: folder.id,
73
- name,
74
- type,
75
- displayPath: path.join(folder.displayPath, name),
76
- locked: isSymlink
77
- });
78
- output.push(toPublicEntry({
79
- ...child,
80
- size: type === 'file' ? stat.size : 0,
81
- modifiedAt: stat.mtime?.toISOString(),
82
- hasChildren: type === 'folder' && !isSymlink
83
- }));
84
- }
85
- output.sort((left, right) => {
86
- if (left.type !== right.type) return left.type === 'folder' ? -1 : 1;
87
- return left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: 'base' });
88
- });
89
- return {
90
- folder: toPublicEntry({ ...folder, hasChildren: true }),
91
- entries: output
92
- };
93
- }
94
-
95
- async function walkDirectory(registry, entry, relativePrefix, output, visited, counters) {
96
- const realPath = await fs.realpath(entry.absolutePath).catch(() => entry.absolutePath);
97
- if (visited.has(realPath)) return;
98
- visited.add(realPath);
99
- let dirents;
100
- try {
101
- dirents = await fs.readdir(entry.absolutePath, { withFileTypes: true });
102
- } catch (error) {
103
- if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
104
- throw error;
105
- }
106
- for (const dirent of dirents) {
107
- const name = safeEntryName(dirent.name);
108
- if (!name || name === '.' || name === '..' || dirent.isSymbolicLink()) continue;
109
- const absolutePath = path.resolve(entry.absolutePath, name);
110
- if (!isPathWithinRoot(entry.rootPath, absolutePath)) continue;
111
- const stat = await readEntryStat(absolutePath);
112
- const relativePath = path.posix.join(relativePrefix, name);
113
- if (stat.isDirectory()) {
114
- counters.folders += 1;
115
- const child = registry.register({
116
- absolutePath,
117
- rootPath: entry.rootPath,
118
- rootId: entry.rootId,
119
- parentId: entry.id,
120
- name,
121
- type: 'folder',
122
- displayPath: path.join(entry.displayPath, name)
123
- });
124
- await walkDirectory(registry, child, relativePath, output, visited, counters);
125
- } else if (stat.isFile()) {
126
- const child = registry.register({
127
- absolutePath,
128
- rootPath: entry.rootPath,
129
- rootId: entry.rootId,
130
- parentId: entry.id,
131
- name,
132
- type: 'file',
133
- displayPath: path.join(entry.displayPath, name)
134
- });
135
- output.push({
136
- id: child.id,
137
- absolutePath,
138
- relativePath,
139
- name,
140
- size: stat.size,
141
- modifiedMs: stat.mtimeMs,
142
- modifiedAt: stat.mtime?.toISOString() || null
143
- });
144
- counters.files += 1;
145
- counters.totalBytes += stat.size;
146
- }
147
- if ((counters.files + counters.folders) % 100 === 0) await yieldToEventLoop();
148
- }
149
- }
150
-
151
- export async function scanSelection(registry, itemIds) {
152
- const ids = [...new Set((Array.isArray(itemIds) ? itemIds : []).map(value => String(value || '').trim()).filter(Boolean))].slice(0, 5000);
153
- if (ids.length === 0) throw filesystemError('no-filesystem-items-selected', 'NO_SELECTION', 400);
154
- const selected = ids.map(id => registry.resolve(id));
155
- const selectedPaths = selected.filter(entry => entry.type === 'folder').map(entry => path.resolve(entry.absolutePath));
156
- const topLevel = selected.filter(entry => !selectedPaths.some(parent => parent !== entry.absolutePath && isPathWithinRoot(parent, entry.absolutePath)));
157
- const files = [];
158
- const counters = { files: 0, folders: 0, totalBytes: 0 };
159
- const visited = new Set();
160
- for (const entry of topLevel) {
161
- const stat = await readEntryStat(entry.absolutePath);
162
- if (entry.type === 'file' || stat.isFile()) {
163
- files.push({
164
- id: entry.id,
165
- absolutePath: entry.absolutePath,
166
- relativePath: entry.name,
167
- name: entry.name,
168
- size: stat.size,
169
- modifiedMs: stat.mtimeMs,
170
- modifiedAt: stat.mtime?.toISOString() || null
171
- });
172
- counters.files += 1;
173
- counters.totalBytes += stat.size;
174
- continue;
175
- }
176
- counters.folders += 1;
177
- await walkDirectory(registry, entry, entry.name, files, visited, counters);
178
- }
179
- files.sort((left, right) => left.relativePath.localeCompare(right.relativePath, undefined, { numeric: true }));
180
- return { ...counters, files };
181
- }
182
-
183
- export function toFilesystemHttpError(error) {
184
- return error?.status
185
- ? error
186
- : filesystemError('filesystem-operation-failed', 'FILESYSTEM_ERROR', 500);
187
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
4
+ import { isPathWithinRoot } from './path-registry.js';
5
+
6
+ function filesystemError(message, code = 'FILESYSTEM_ERROR', status = 400) {
7
+ const error = new Error(message);
8
+ error.code = code;
9
+ error.status = status;
10
+ return error;
11
+ }
12
+
13
+ function safeEntryName(name) {
14
+ return String(name || '').replace(/[\0\r\n\t]/g, ' ').slice(0, 512);
15
+ }
16
+
17
+ function toPublicEntry(entry) {
18
+ return {
19
+ id: entry.id,
20
+ parentId: entry.parentId,
21
+ name: entry.name,
22
+ type: entry.type,
23
+ size: entry.size || 0,
24
+ modifiedAt: entry.modifiedAt || null,
25
+ extension: entry.type === 'file' ? path.extname(entry.name).toLowerCase() : '',
26
+ hasChildren: entry.type !== 'file' && entry.hasChildren === true,
27
+ displayPath: entry.displayPath,
28
+ locked: entry.locked === true
29
+ };
30
+ }
31
+
32
+ async function readEntryStat(absolutePath) {
33
+ try {
34
+ return await fs.lstat(absolutePath);
35
+ } catch (error) {
36
+ if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
37
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') throw filesystemError('filesystem-entry-unavailable', 'NOT_FOUND', 404);
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ export async function readDirectoryEntries(registry, folderId) {
43
+ const folder = registry.resolve(folderId);
44
+ if (folder.type === 'file') throw filesystemError('filesystem-entry-is-not-folder', 'NOT_A_FOLDER', 400);
45
+ if (folder.locked) throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
46
+ let entries;
47
+ try {
48
+ entries = await fs.readdir(folder.absolutePath, { withFileTypes: true });
49
+ } catch (error) {
50
+ if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
51
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') throw filesystemError('filesystem-entry-unavailable', 'NOT_FOUND', 404);
52
+ throw error;
53
+ }
54
+
55
+ const output = [];
56
+ for (const dirent of entries) {
57
+ const name = safeEntryName(dirent.name);
58
+ if (!name || name === '.' || name === '..') continue;
59
+ const absolutePath = path.resolve(folder.absolutePath, name);
60
+ if (!isPathWithinRoot(folder.rootPath, absolutePath)) continue;
61
+ const stat = await readEntryStat(absolutePath).catch(error => {
62
+ if (error?.code === 'ACCESS_DENIED' || error?.code === 'NOT_FOUND') return null;
63
+ throw error;
64
+ });
65
+ if (!stat) continue;
66
+ const isSymlink = dirent.isSymbolicLink() || stat.isSymbolicLink();
67
+ const type = !isSymlink && (dirent.isDirectory() || stat.isDirectory()) ? 'folder' : 'file';
68
+ const child = registry.register({
69
+ absolutePath,
70
+ rootPath: folder.rootPath,
71
+ rootId: folder.rootId,
72
+ parentId: folder.id,
73
+ name,
74
+ type,
75
+ displayPath: path.join(folder.displayPath, name),
76
+ locked: isSymlink
77
+ });
78
+ output.push(toPublicEntry({
79
+ ...child,
80
+ size: type === 'file' ? stat.size : 0,
81
+ modifiedAt: stat.mtime?.toISOString(),
82
+ hasChildren: type === 'folder' && !isSymlink
83
+ }));
84
+ }
85
+ output.sort((left, right) => {
86
+ if (left.type !== right.type) return left.type === 'folder' ? -1 : 1;
87
+ return left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: 'base' });
88
+ });
89
+ return {
90
+ folder: toPublicEntry({ ...folder, hasChildren: true }),
91
+ entries: output
92
+ };
93
+ }
94
+
95
+ async function walkDirectory(registry, entry, relativePrefix, output, visited, counters) {
96
+ const realPath = await fs.realpath(entry.absolutePath).catch(() => entry.absolutePath);
97
+ if (visited.has(realPath)) return;
98
+ visited.add(realPath);
99
+ let dirents;
100
+ try {
101
+ dirents = await fs.readdir(entry.absolutePath, { withFileTypes: true });
102
+ } catch (error) {
103
+ if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
104
+ throw error;
105
+ }
106
+ for (const dirent of dirents) {
107
+ const name = safeEntryName(dirent.name);
108
+ if (!name || name === '.' || name === '..' || dirent.isSymbolicLink()) continue;
109
+ const absolutePath = path.resolve(entry.absolutePath, name);
110
+ if (!isPathWithinRoot(entry.rootPath, absolutePath)) continue;
111
+ const stat = await readEntryStat(absolutePath);
112
+ const relativePath = path.posix.join(relativePrefix, name);
113
+ if (stat.isDirectory()) {
114
+ counters.folders += 1;
115
+ const child = registry.register({
116
+ absolutePath,
117
+ rootPath: entry.rootPath,
118
+ rootId: entry.rootId,
119
+ parentId: entry.id,
120
+ name,
121
+ type: 'folder',
122
+ displayPath: path.join(entry.displayPath, name)
123
+ });
124
+ await walkDirectory(registry, child, relativePath, output, visited, counters);
125
+ } else if (stat.isFile()) {
126
+ const child = registry.register({
127
+ absolutePath,
128
+ rootPath: entry.rootPath,
129
+ rootId: entry.rootId,
130
+ parentId: entry.id,
131
+ name,
132
+ type: 'file',
133
+ displayPath: path.join(entry.displayPath, name)
134
+ });
135
+ output.push({
136
+ id: child.id,
137
+ absolutePath,
138
+ relativePath,
139
+ name,
140
+ size: stat.size,
141
+ modifiedMs: stat.mtimeMs,
142
+ modifiedAt: stat.mtime?.toISOString() || null
143
+ });
144
+ counters.files += 1;
145
+ counters.totalBytes += stat.size;
146
+ }
147
+ if ((counters.files + counters.folders) % 100 === 0) await yieldToEventLoop();
148
+ }
149
+ }
150
+
151
+ export async function scanSelection(registry, itemIds) {
152
+ const ids = [...new Set((Array.isArray(itemIds) ? itemIds : []).map(value => String(value || '').trim()).filter(Boolean))].slice(0, 5000);
153
+ if (ids.length === 0) throw filesystemError('no-filesystem-items-selected', 'NO_SELECTION', 400);
154
+ const selected = ids.map(id => registry.resolve(id));
155
+ const selectedPaths = selected.filter(entry => entry.type === 'folder').map(entry => path.resolve(entry.absolutePath));
156
+ const topLevel = selected.filter(entry => !selectedPaths.some(parent => parent !== entry.absolutePath && isPathWithinRoot(parent, entry.absolutePath)));
157
+ const files = [];
158
+ const counters = { files: 0, folders: 0, totalBytes: 0 };
159
+ const visited = new Set();
160
+ for (const entry of topLevel) {
161
+ const stat = await readEntryStat(entry.absolutePath);
162
+ if (entry.type === 'file' || stat.isFile()) {
163
+ files.push({
164
+ id: entry.id,
165
+ absolutePath: entry.absolutePath,
166
+ relativePath: entry.name,
167
+ name: entry.name,
168
+ size: stat.size,
169
+ modifiedMs: stat.mtimeMs,
170
+ modifiedAt: stat.mtime?.toISOString() || null
171
+ });
172
+ counters.files += 1;
173
+ counters.totalBytes += stat.size;
174
+ continue;
175
+ }
176
+ counters.folders += 1;
177
+ await walkDirectory(registry, entry, entry.name, files, visited, counters);
178
+ }
179
+ files.sort((left, right) => left.relativePath.localeCompare(right.relativePath, undefined, { numeric: true }));
180
+ return { ...counters, files };
181
+ }
182
+
183
+ export function toFilesystemHttpError(error) {
184
+ return error?.status
185
+ ? error
186
+ : filesystemError('filesystem-operation-failed', 'FILESYSTEM_ERROR', 500);
187
+ }