@vo1d7bin/remote-mcp-agent 0.1.0 → 0.2.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 (3) hide show
  1. package/README.md +42 -42
  2. package/bin/vo1d-agent.mjs +195 -112
  3. package/package.json +32 -32
package/README.md CHANGED
@@ -1,42 +1,42 @@
1
- # vo1d Remote MCP device agent
2
-
3
- Independent, open CLI for connecting an authorized Windows, Linux or macOS device to **your own** account at https://mcp.thelinuxserver.cloud. This is **not** the third-party Desktop Commander relay.
4
-
5
- Requires Node.js 22.14 or newer.
6
-
7
- ### Pair this machine
8
-
9
- Sign in to https://mcp.thelinuxserver.cloud/dashboard and choose **Pair a device**. Copy the one-time code. Run:
10
-
11
- ```powershell
12
- npx -y @vo1d7bin/remote-mcp-agent@latest enroll
13
- ```
14
-
15
- Paste the code when prompted. The credentials are saved outside your source tree at `~/.vo1d-remote-mcp/device.json`. Never share this file.
16
-
17
- ### Start the agent
18
-
19
- ```powershell
20
- npx -y @vo1d7bin/remote-mcp-agent@latest remote --allow-root "C:\Users\Public\Documents"
21
- ```
22
-
23
- For Linux: `npx -y @vo1d7bin/remote-mcp-agent@latest remote --allow-root /home/user/mcp-share`.
24
-
25
- Use `VO1D_ALLOWED_ROOTS` to set multiple allowed paths, separated by your system PATH delimiter (semicolon on Windows, colon on Linux). Without allowed roots the agent only returns basic system information. An allowed root may expose file and directory names within that folder to the account's explicitly approved MCP clients. Only select directories you're comfortable exposing.
26
-
27
- ### Useful options
28
-
29
- ```text
30
- enroll [--server https://mcp.thelinuxserver.cloud] [--name YOUR-PC]
31
- remote [--allow-root PATH]
32
- ```
33
-
34
- For a second agent profile, set `VO1D_CONFIG_DIR` before enrollment and when starting its `remote` process.
35
-
36
- The initial release supports only `system_info` and `list_directory`. It cannot read file contents, write files, or run shell commands. It reconnects automatically while its process is running. To revoke credentials, open the vo1d Devices dashboard and select **Revoke**.
37
-
38
- ### Security
39
-
40
- Only install this package from the official npm registry under the `@vo1d7bin` scope. Do not run agents as Administrator/root unless necessary. Do not forward or publish the device token. Use explicit, limited allowlisted roots and review client permissions.
41
-
42
- Maintained source: https://github.com/THE-L1NUX-USER/vo1d-remote-mcp/tree/main/agent
1
+ # vo1d Remote MCP device agent
2
+
3
+ Connect your own Windows, Linux, or macOS computer to your account on **https://mcp.thelinuxserver.cloud** using one command. This agent is independent of Desktop Commander and its hosted relay.
4
+
5
+ Requires Node.js 22.14 or newer.
6
+
7
+ ## One-command connection
8
+
9
+ ```powershell
10
+ npx -y @vo1d7bin/remote-mcp-agent@latest remote
11
+ ```
12
+
13
+ **First run:** The agent prints a verification code and opens your default browser. Sign in with Google if needed, compare the code shown in the terminal and browser, then select **Approve device**. The agent automatically receives device credentials, stores them securely in your user profile, and connects to our relay.
14
+
15
+ **Later runs:** Execute the **same command**. Previously saved device credentials are reused, so no browser approval is required. The agent reconnects automatically after temporary connection interruptions while it remains running.
16
+
17
+ The terminal must remain open for the agent to remain online. We may add an installer/service or desktop tray application in a future version.
18
+
19
+ By default, the agent permits **system information only**. To additionally expose names of files and directories within a specific, safe folder, use:
20
+
21
+ ```powershell
22
+ npx -y @vo1d7bin/remote-mcp-agent@latest remote --allow-root "$HOME\mcp-share"
23
+ ```
24
+
25
+ The specified directory must already exist. Do not grant access to your full home directory, system drive, or secrets. Only the folder and its descendants are visible. No file contents, writes, or terminal execution are provided in this version.
26
+
27
+ ## Advanced options
28
+
29
+ ```text
30
+ vo1d-agent remote [--allow-root PATH] [--server https://mcp.thelinuxserver.cloud]
31
+ vo1d-agent enroll [--server https://mcp.thelinuxserver.cloud]
32
+ ```
33
+
34
+ For a headless machine, run with `--no-browser`; the printed link can be opened from a browser on any other device. Use `VO1D_ALLOWED_ROOTS` for multiple approved folders, separated by your OS path delimiter (semicolon on Windows, colon on Linux).
35
+
36
+ For a separate agent identity on the same computer, set `VO1D_CONFIG_DIR` to an independent directory **before** running `remote`. Device credentials are stored at `~/.vo1d-remote-mcp/device.json` by default. Keep this private; never commit it to source control.
37
+
38
+ The legacy `enroll` command is available for manual one-time pairing codes. Open **Devices → Pair a device → Manual pairing** if the browser-based flow cannot be used.
39
+
40
+ Device access can be revoked at any time in your vo1d dashboard. If revoked, the stored credentials cannot reconnect and the machine must be authorized again.
41
+
42
+ Source: https://github.com/THE-L1NUX-USER/vo1d-remote-mcp
@@ -1,112 +1,195 @@
1
- #!/usr/bin/env node
2
- import WebSocket from 'ws';
3
- import os from 'node:os';
4
- import { promises as fs } from 'node:fs';
5
- import path from 'node:path';
6
- import readline from 'node:readline/promises';
7
-
8
- const folder = process.env.VO1D_CONFIG_DIR ? path.resolve(process.env.VO1D_CONFIG_DIR) : path.join(os.homedir(), '.vo1d-remote-mcp');
9
- const configFile = path.join(folder, 'device.json');
10
- const arg = (name) => { const i = process.argv.indexOf(name); return i < 0 ? undefined : process.argv[i + 1]; };
11
- function normalizedServer(raw) {
12
- const u = new URL(raw);
13
- if (u.protocol !== 'https:' && !(u.protocol === 'http:' &&
14
- ['127.0.0.1', 'localhost'].includes(u.hostname))) {
15
- throw new Error('An HTTPS server URL is required outside localhost');
16
- }
17
- return u.origin;
18
- }
19
- async function enroll() {
20
- const server = normalizedServer(arg('--server') || process.env.VO1D_SERVER || 'https://mcp.thelinuxserver.cloud');
21
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
22
- let code;
23
- try { code = (await rl.question('Paste the one-time pairing code: ')).trim(); }
24
- finally { rl.close(); }
25
- const res = await fetch(server + '/api/agent/enroll', {
26
- method: 'POST', headers: { 'content-type': 'application/json' },
27
- body: JSON.stringify({ code, name: arg('--name') || os.hostname(), platform: process.platform })
28
- });
29
- if (!res.ok) throw new Error('Device pairing failed (HTTP ' + res.status + ')');
30
- const { id, token } = await res.json();
31
- await fs.mkdir(folder, { recursive: true, mode: 0o700 });
32
- try {
33
- await fs.writeFile(configFile, JSON.stringify({ server, id, token }, null, 2), { mode: 0o600, flag: 'wx' });
34
- } catch(error) {
35
- if (error.code === 'EEXIST') {
36
- throw new Error('A device is already enrolled in this profile. Run "remote" or specify VO1D_CONFIG_DIR to use a separate profile.');
37
- }
38
- throw error;
39
- }
40
- console.log('Paired successfully. Device ID:', id);
41
- console.log('Credentials saved outside the Git repository:', configFile);
42
- console.log('Run: node agent/index.mjs remote');
43
- }
44
- async function allowedRoots() {
45
- const provided = process.env.VO1D_ALLOWED_ROOTS || arg('--allow-root') || '';
46
- const paths = provided.split(path.delimiter).map(s => s.trim()).filter(Boolean);
47
- const roots = [];
48
- for (const p of paths) roots.push(await fs.realpath(path.resolve(p)));
49
- return roots;
50
- }
51
- function includesRoot(p, root) {
52
- const value = process.platform === 'win32' ? p.toLowerCase() : p;
53
- const base = process.platform === 'win32' ? root.toLowerCase() : root;
54
- return value === base || value.startsWith(base.endsWith(path.sep) ? base : base + path.sep);
55
- }
56
- async function directoryListing(requestPath, roots) {
57
- if (!roots.length) throw new Error('No allowed roots configured; set VO1D_ALLOWED_ROOTS');
58
- if (typeof requestPath !== 'string' || requestPath.length > 2000) throw new Error('Invalid directory path');
59
- const resolved = await fs.realpath(path.resolve(requestPath));
60
- if (!roots.some(root => includesRoot(resolved, root))) throw new Error('Path outside allowed directories');
61
- const entries = await fs.readdir(resolved, { withFileTypes: true });
62
- return { path: resolved, truncated: entries.length > 100, items: entries.slice(0, 100)
63
- .map(entry => ({ name: entry.name, type: entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : 'other' })) };
64
- }
65
- async function remote() {
66
- const cfg = JSON.parse(await fs.readFile(configFile, 'utf8'));
67
- const server = normalizedServer(cfg.server);
68
- const roots = await allowedRoots();
69
- console.log('Device:', cfg.id, '| Allowed filesystem roots:', roots.length ? roots.join(' ; ') : '(none)');
70
- const endpoint = server.replace(/^http/, 'ws') + '/agent';
71
- let stopped = false;
72
- let retry = 1000;
73
- process.on('SIGINT', () => { stopped = true; process.exit(0); });
74
- async function connect() {
75
- if (stopped) return;
76
- const ws = new WebSocket(endpoint, { headers: { Authorization: 'Bearer ' + cfg.token }, handshakeTimeout: 12000 });
77
- ws.on('open', () => { retry = 1000; console.log('Connected to', server); });
78
- ws.on('message', async raw => {
79
- let task;
80
- try { task = JSON.parse(raw.toString()); } catch { return; }
81
- if (typeof task.id !== 'string') return;
82
- let reply;
83
- try {
84
- if (task.action === 'system_info') {
85
- reply = { hostname: os.hostname(), platform: process.platform, arch: process.arch,
86
- release: os.release(), cpus: os.cpus().length, memoryBytes: os.totalmem(), allowedRoots: roots };
87
- } else if (task.action === 'list_directory') {
88
- reply = await directoryListing(task.args?.path, roots);
89
- } else {
90
- throw new Error('Unsupported action');
91
- }
92
- ws.send(JSON.stringify({ id: task.id, ok: true, result: reply }));
93
- } catch (error) {
94
- ws.send(JSON.stringify({ id: task.id, ok: false, error: String(error.message).slice(0, 150) }));
95
- }
96
- });
97
- ws.on('error', error => console.error('Connection:', error.message));
98
- ws.on('close', (code) => {
99
- if (code === 4001) { console.error('Device revoked. Exiting.'); stopped = true; return; }
100
- if (stopped) return;
101
- const delay = retry;
102
- retry = Math.min(retry * 2, 30000);
103
- console.log('Disconnected. Retrying in', delay / 1000, 'seconds.');
104
- setTimeout(connect, delay);
105
- });
106
- }
107
- await connect();
108
- }
109
- const command = process.argv[2];
110
- if (command === 'enroll') enroll().catch(e => { console.error(e.message); process.exitCode = 1; });
111
- else if (command === 'remote') remote().catch(e => { console.error(e.message); process.exitCode = 1; });
112
- else console.log('Usage: vo1d-agent enroll [--server https://mcp.thelinuxserver.cloud] [--name MY-PC]\n vo1d-agent remote [--allow-root PATH]\nPass VO1D_ALLOWED_ROOTS for multiple roots or VO1D_CONFIG_DIR for separate profiles.');
1
+ #!/usr/bin/env node
2
+ import WebSocket from 'ws';
3
+ import os from 'node:os';
4
+ import { promises as fs } from 'node:fs';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline/promises';
7
+ import { spawn } from 'node:child_process';
8
+ import { setTimeout as sleep } from 'node:timers/promises';
9
+
10
+ const folder = process.env.VO1D_CONFIG_DIR
11
+ ? path.resolve(process.env.VO1D_CONFIG_DIR)
12
+ : path.join(os.homedir(), '.vo1d-remote-mcp');
13
+ const configFile = path.join(folder, 'device.json');
14
+ const arg = name => {
15
+ const i = process.argv.indexOf(name);
16
+ return i < 0 ? undefined : process.argv[i + 1];
17
+ };
18
+ function normalizedServer(raw) {
19
+ const url = new URL(raw);
20
+ if (url.protocol !== 'https:' &&
21
+ !(url.protocol === 'http:' && ['127.0.0.1','localhost'].includes(url.hostname))) {
22
+ throw new Error('HTTPS is required outside localhost');
23
+ }
24
+ if (url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
25
+ throw new Error('Expected a server origin, not a URL with a path or credentials');
26
+ }
27
+ return url.origin;
28
+ }
29
+ function defaultServer() {
30
+ return normalizedServer(arg('--server') || process.env.VO1D_SERVER || 'https://mcp.thelinuxserver.cloud');
31
+ }
32
+ async function saveDevice(cfg) {
33
+ await fs.mkdir(folder, {recursive:true, mode:0o700});
34
+ try {
35
+ await fs.writeFile(configFile, JSON.stringify(cfg,null,2), {mode:0o600,flag:'wx'});
36
+ } catch(error) {
37
+ if(error.code==='EEXIST')throw new Error('Device already enrolled in this profile; use remote to reconnect');
38
+ throw error;
39
+ }
40
+ }
41
+ async function legacyEnroll() {
42
+ const server=defaultServer();
43
+ const rl=readline.createInterface({input:process.stdin,output:process.stdout});
44
+ let code;
45
+ try {code=(await rl.question('Paste the one-time pairing code: ')).trim();}
46
+ finally {rl.close();}
47
+ const reply=await fetch(server+'/api/agent/enroll',{
48
+ method:'POST',headers:{'content-type':'application/json'},
49
+ body:JSON.stringify({code,name:arg('--name')||os.hostname(),platform:process.platform})
50
+ });
51
+ if(!reply.ok)throw new Error('Device pairing failed (HTTP '+reply.status+')');
52
+ const {id,token}=await reply.json();
53
+ await saveDevice({server,id,token});
54
+ console.log('Paired successfully. Device ID:',id);
55
+ console.log('To connect, run: vo1d-agent remote');
56
+ }
57
+ function openBrowser(address) {
58
+ if (process.argv.includes('--no-browser') || process.env.VO1D_NO_BROWSER==='true') return false;
59
+ const program=process.platform==='win32'?'explorer.exe':process.platform==='darwin'?'open':'xdg-open';
60
+ try {
61
+ const child=spawn(program,[address],{stdio:'ignore',detached:true,windowsHide:true});
62
+ child.on('error',()=>{}); // Link is always printed for environments without a browser.
63
+ child.unref();
64
+ return true;
65
+ } catch {return false;}
66
+ }
67
+ async function browserEnroll(server) {
68
+ const response=await fetch(server+'/api/agent/authorization/start',{
69
+ method:'POST',headers:{'content-type':'application/json'},
70
+ body:JSON.stringify({name:arg('--name')||os.hostname(),platform:process.platform})
71
+ });
72
+ if(!response.ok)throw new Error('Could not start browser authorization (HTTP '+response.status+')');
73
+ const data=await response.json();
74
+ if(typeof data.requestId!=='string' || typeof data.pollToken!=='string')throw new Error('Unexpected authorization response');
75
+ const link=new URL(data.verificationUrl);
76
+ if(link.origin!==server || link.pathname!=='/device/authorize')throw new Error('Unexpected authorization destination');
77
+ console.log('\nFirst-time connection: approve this device in your web browser.');
78
+ console.log('Verification code:',data.userCode);
79
+ console.log('Open:',link.href);
80
+ openBrowser(link.href);
81
+ console.log('Waiting for approval (expires in 10 minutes). Press Ctrl+C to cancel.\n');
82
+ const deadline=Math.min(Number(data.expiresAt),Date.now()+10*60_000);
83
+ let failure=0;
84
+ while(Date.now()<deadline) {
85
+ await sleep(Math.max(2000,Number(data.intervalSeconds||3)*1000));
86
+ let poll;
87
+ try {
88
+ poll=await fetch(server+'/api/agent/authorization/poll',{
89
+ method:'POST',headers:{'content-type':'application/json'},
90
+ body:JSON.stringify({requestId:data.requestId,pollToken:data.pollToken}),
91
+ signal:AbortSignal.timeout(12000)
92
+ });
93
+ } catch(error) {
94
+ failure++;
95
+ if(failure>=5)throw new Error('Unable to reach the authorization server: '+error.message);
96
+ continue;
97
+ }
98
+ failure=0;
99
+ const info=await poll.json().catch(()=>({}));
100
+ if(info.status==='pending')continue;
101
+ if(info.status==='approved' && typeof info.id==='string' && typeof info.token==='string') {
102
+ const cfg={server,id:info.id,token:info.token};
103
+ await saveDevice(cfg);
104
+ console.log('Device approved and credentials stored securely for future runs.');
105
+ return cfg;
106
+ }
107
+ if(info.status==='denied')throw new Error('This device authorization was denied');
108
+ if(info.status==='expired' || poll.status===410)throw new Error('Authorization expired; rerun the command');
109
+ throw new Error('Authorization failed (HTTP '+poll.status+'). Rerun the command.');
110
+ }
111
+ throw new Error('Device authorization expired; rerun the command');
112
+ }
113
+ async function loadOrEnroll() {
114
+ try {
115
+ const cfg=JSON.parse(await fs.readFile(configFile,'utf8'));
116
+ if(typeof cfg.id!=='string' || typeof cfg.token!=='string' || cfg.token.length<30) {
117
+ throw new Error('Stored device credentials appear corrupted');
118
+ }
119
+ return cfg;
120
+ } catch(error) {
121
+ if(error.code!=='ENOENT')throw error;
122
+ return browserEnroll(defaultServer());
123
+ }
124
+ }
125
+ async function allowedRoots() {
126
+ const provided=process.env.VO1D_ALLOWED_ROOTS || arg('--allow-root') || '';
127
+ const paths=provided.split(path.delimiter).map(x=>x.trim()).filter(Boolean);
128
+ const roots=[];
129
+ for(const p of paths)roots.push(await fs.realpath(path.resolve(p)));
130
+ return roots;
131
+ }
132
+ function includesRoot(p,root) {
133
+ const value=process.platform==='win32'?p.toLowerCase():p;
134
+ const base=process.platform==='win32'?root.toLowerCase():root;
135
+ return value===base || value.startsWith(base.endsWith(path.sep)?base:base+path.sep);
136
+ }
137
+ async function directoryListing(requestPath,roots) {
138
+ if(!roots.length)throw new Error('No allowed roots configured; set VO1D_ALLOWED_ROOTS');
139
+ if(typeof requestPath!=='string' || requestPath.length>2000)throw new Error('Invalid directory path');
140
+ const resolved=await fs.realpath(path.resolve(requestPath));
141
+ if(!roots.some(root=>includesRoot(resolved,root)))throw new Error('Path outside allowed directories');
142
+ const entries=await fs.readdir(resolved,{withFileTypes:true});
143
+ return {path:resolved,truncated:entries.length>100,items:entries.slice(0,100).map(entry=>({
144
+ name:entry.name,type:entry.isDirectory()?'directory':entry.isFile()?'file':'other'
145
+ }))};
146
+ }
147
+ async function remote() {
148
+ const roots=await allowedRoots();
149
+ const cfg=await loadOrEnroll();
150
+ const server=normalizedServer(cfg.server);
151
+ console.log('Device:',cfg.id,'| Allowed filesystem roots:',roots.length?roots.join(' ; '):'(none)');
152
+ const endpoint=server.replace(/^http/,'ws')+'/agent';
153
+ let stopped=false,retry=1000;
154
+ process.on('SIGINT',()=>{stopped=true;process.exit(0);});
155
+ async function connect() {
156
+ if(stopped)return;
157
+ const ws=new WebSocket(endpoint,{headers:{Authorization:'Bearer '+cfg.token},handshakeTimeout:12000});
158
+ ws.on('open',()=>{retry=1000;console.log('Connected to',server);});
159
+ ws.on('message',async raw=>{
160
+ let task;
161
+ try{task=JSON.parse(raw.toString());}catch{return;}
162
+ if(typeof task.id!=='string')return;
163
+ try {
164
+ let result;
165
+ if(task.action==='system_info'){
166
+ result={hostname:os.hostname(),platform:process.platform,arch:process.arch,
167
+ release:os.release(),cpus:os.cpus().length,memoryBytes:os.totalmem(),allowedRoots:roots};
168
+ }else if(task.action==='list_directory'){
169
+ result=await directoryListing(task.args?.path,roots);
170
+ }else throw new Error('Unsupported action');
171
+ if(ws.readyState===WebSocket.OPEN)ws.send(JSON.stringify({id:task.id,ok:true,result}));
172
+ }catch(error){
173
+ if(ws.readyState===WebSocket.OPEN)ws.send(JSON.stringify({
174
+ id:task.id,ok:false,error:String(error.message).slice(0,150)
175
+ }));
176
+ }
177
+ });
178
+ ws.on('error',error=>console.error('Connection:',error.message));
179
+ ws.on('close',code=>{
180
+ if(code===4000){console.log('Another session for this device has taken over. Exiting this agent.');stopped=true;return;}
181
+ if(code===4001){console.error('Device revoked. Stop this process and request a new authorization.');stopped=true;return;}
182
+ if(stopped)return;
183
+ const delay=retry;retry=Math.min(retry*2,30000);
184
+ console.log('Disconnected. Retrying in',delay/1000,'seconds.');
185
+ setTimeout(connect,delay);
186
+ });
187
+ }
188
+ await connect();
189
+ }
190
+ const command=process.argv[2];
191
+ if(command==='enroll')legacyEnroll().catch(error=>{console.error(error.message);process.exitCode=1;});
192
+ else if(command==='remote')remote().catch(error=>{console.error(error.message);process.exitCode=1;});
193
+ else console.log('Usage: vo1d-agent remote [--allow-root PATH] [--server https://mcp.thelinuxserver.cloud]\n' +
194
+ ' vo1d-agent enroll (manual pairing for headless environments)\n' +
195
+ 'The remote command opens a browser on first run and reconnects on subsequent runs.');
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
- {
2
- "name": "@vo1d7bin/remote-mcp-agent",
3
- "version": "0.1.0",
4
- "description": "Independent vo1d Remote MCP device agent for Windows, Linux and macOS",
5
- "type": "module",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/THE-L1NUX-USER/vo1d-remote-mcp.git",
10
- "directory": "packages/agent"
11
- },
12
- "homepage": "https://mcp.thelinuxserver.cloud",
13
- "bin": {
14
- "vo1d-agent": "bin/vo1d-agent.mjs"
15
- },
16
- "files": [
17
- "bin/",
18
- "README.md"
19
- ],
20
- "engines": {
21
- "node": ">=22.14.0"
22
- },
23
- "dependencies": {
24
- "ws": "8.18.3"
25
- },
26
- "scripts": {
27
- "prepack": "node ../../scripts/sync-agent.mjs"
28
- },
29
- "publishConfig": {
30
- "access": "public"
31
- }
32
- }
1
+ {
2
+ "name": "@vo1d7bin/remote-mcp-agent",
3
+ "version": "0.2.0",
4
+ "description": "Independent vo1d Remote MCP device agent for Windows, Linux and macOS",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/THE-L1NUX-USER/vo1d-remote-mcp.git",
10
+ "directory": "packages/agent"
11
+ },
12
+ "homepage": "https://mcp.thelinuxserver.cloud",
13
+ "bin": {
14
+ "vo1d-agent": "bin/vo1d-agent.mjs"
15
+ },
16
+ "files": [
17
+ "bin/",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22.14.0"
22
+ },
23
+ "dependencies": {
24
+ "ws": "8.18.3"
25
+ },
26
+ "scripts": {
27
+ "prepack": "node ../../scripts/sync-agent.mjs"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }