remote-access-mcp 2.1.2 → 2.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 (56) hide show
  1. package/dist/cli/main.d.ts.map +1 -1
  2. package/dist/cli/main.js +566 -359
  3. package/dist/cli/main.js.map +1 -1
  4. package/dist/core/config.d.ts +7 -0
  5. package/dist/core/config.d.ts.map +1 -1
  6. package/dist/core/config.js +37 -2
  7. package/dist/core/config.js.map +1 -1
  8. package/dist/core/platform.d.ts +49 -0
  9. package/dist/core/platform.d.ts.map +1 -0
  10. package/dist/core/platform.js +123 -0
  11. package/dist/core/platform.js.map +1 -0
  12. package/dist/core/policy.d.ts +2 -0
  13. package/dist/core/policy.d.ts.map +1 -1
  14. package/dist/core/policy.js +14 -5
  15. package/dist/core/policy.js.map +1 -1
  16. package/dist/core/tunnel.d.ts +23 -0
  17. package/dist/core/tunnel.d.ts.map +1 -0
  18. package/dist/core/tunnel.js +141 -0
  19. package/dist/core/tunnel.js.map +1 -0
  20. package/dist/server/app.d.ts +2 -0
  21. package/dist/server/app.d.ts.map +1 -1
  22. package/dist/server/app.js +104 -38
  23. package/dist/server/app.js.map +1 -1
  24. package/dist/server/run.d.ts +1 -0
  25. package/dist/server/run.d.ts.map +1 -1
  26. package/dist/server/run.js +48 -14
  27. package/dist/server/run.js.map +1 -1
  28. package/dist/server/sessions.d.ts +38 -0
  29. package/dist/server/sessions.d.ts.map +1 -0
  30. package/dist/server/sessions.js +62 -0
  31. package/dist/server/sessions.js.map +1 -0
  32. package/dist/tools/logs.d.ts.map +1 -1
  33. package/dist/tools/logs.js +21 -4
  34. package/dist/tools/logs.js.map +1 -1
  35. package/dist/tools/packages.d.ts.map +1 -1
  36. package/dist/tools/packages.js +82 -26
  37. package/dist/tools/packages.js.map +1 -1
  38. package/dist/tools/planning.d.ts.map +1 -1
  39. package/dist/tools/planning.js +3 -3
  40. package/dist/tools/planning.js.map +1 -1
  41. package/dist/tools/schedule.d.ts.map +1 -1
  42. package/dist/tools/schedule.js +2 -2
  43. package/dist/tools/schedule.js.map +1 -1
  44. package/dist/tools/security.d.ts.map +1 -1
  45. package/dist/tools/security.js +15 -2
  46. package/dist/tools/security.js.map +1 -1
  47. package/dist/tools/services.d.ts.map +1 -1
  48. package/dist/tools/services.js +45 -14
  49. package/dist/tools/services.js.map +1 -1
  50. package/dist/tools/shell.d.ts.map +1 -1
  51. package/dist/tools/shell.js +32 -12
  52. package/dist/tools/shell.js.map +1 -1
  53. package/dist/tools/system.d.ts.map +1 -1
  54. package/dist/tools/system.js +18 -9
  55. package/dist/tools/system.js.map +1 -1
  56. package/package.json +1 -1
package/dist/cli/main.js CHANGED
@@ -1,78 +1,83 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import net from 'node:net';
4
5
  import { execFileSync } from 'node:child_process';
5
6
  import { fileURLToPath } from 'node:url';
6
- import net from 'node:net';
7
7
  import { loadConfig, saveConfig, generateToken, newTokenRecord, configPath, configDir, primaryToken, } from '../core/config.js';
8
- import { resolveReal } from '../core/policy.js';
8
+ import { resolveReal, TOOL_SCOPES } from '../core/policy.js';
9
9
  import { AuditLog } from '../core/audit.js';
10
+ import { platform, isWindows, isMac, isLinux, hasSystemd, platformLabel, } from '../core/platform.js';
11
+ import { resolveCloudflared } from '../core/tunnel.js';
10
12
  const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
11
13
  const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
12
14
  // ---------------------------------------------------------------------------
13
15
  // help
14
16
  // ---------------------------------------------------------------------------
15
- const HELP = `${PKG.name} v${PKG.version} — turn any Linux server into an AI-agent-accessible machine
17
+ const HELP = `${PKG.name} v${PKG.version} — turn any machine into an AI-agent-accessible endpoint
18
+ Runs on Linux, macOS, and Windows.
16
19
 
17
20
  Usage:
18
21
  ramcp <command> [options]
19
22
 
20
- Commands:
21
- init Generate config + first token (safe to re-run)
22
- start [--host H] [--port P]
23
- [--read-only] Run the gateway in the foreground
24
- url [--token ID|name] Print the MCP connector URL for chatbots
25
- status Service status summary
26
- doctor Diagnose token, port, nginx, DNS in one pass
27
- version Print version
23
+ Getting started:
24
+ init [--paths a,b] Create config + first token
25
+ start [--tunnel] Run the gateway (--tunnel = public https URL)
26
+ tunnel Start gateway + public URL, print connector link
27
+ url [token] Print the connector URL for a chatbot
28
+ doctor Diagnose everything in one pass
29
+ status Config + service summary
30
+ upgrade [--dry-run] Self-update from npm
31
+ version
28
32
 
29
- token list [--json] List tokens (fingerprints only)
30
- token add --name N [opts] Create a token (see options below)
31
- token show [ID|name] [--full] Show a token
32
- token rotate [ID|name] Rotate a token (old one dies instantly)
33
- token revoke ID|name Revoke (delete) a token
34
- token add options:
35
- --shell / --no-shell shell execution (default: off)
36
- --paths a,b,c allowed paths (comma-separated)
37
- --deny a,b denied paths
38
- --scopes group,group tool groups (empty = all)
39
- --read-only refuse mutating tools
40
- --rpm N max requests per minute
41
- --expires YYYY-MM-DD expiry date
33
+ Paths the AI may touch (per token):
34
+ policy [--token N] Show policy
35
+ policy allow <path...> [--token N] Allow one or more directories
36
+ policy deny <path...> [--token N] Deny directories
37
+ policy shell on|off [--token N] Toggle shell execution
38
+ policy scopes <groups> [--token N] Limit to tool groups (or 'all')
39
+ policy readonly on|off Global read-only kill-switch
42
40
 
43
- policy [ID|name] Show a token's policy
44
- policy allow <path> Allow a path (default token)
45
- policy deny <path> Deny a path
46
- policy shell on|off Enable/disable shell (default token)
47
- policy readonly on|off Global read-only kill-switch
41
+ Tokens:
42
+ token list [--json]
43
+ token add --name N [--paths a,b] [--shell] [--scopes g,g]
44
+ [--read-only] [--rpm N] [--expires YYYY-MM-DD]
45
+ token show [N] [--full] | token rotate [N] | token revoke N
48
46
 
49
- audit [query] [--tool T] [--since ISO] [--limit N]
50
- [--verify] [--json] Query the tamper-evident audit log
51
- audit chain Verify hash-chain integrity
47
+ Audit:
48
+ audit [--tool T] [--since ISO] [--limit N] [--json]
49
+ audit chain Verify tamper-evident hash chain
52
50
 
53
- service install [--domain D] Install systemd unit (+ nginx vhost)
54
- service uninstall Remove service + nginx vhost
55
- service logs [-f] Tail gateway logs
56
- service status Detailed service check
51
+ Service (autostart):
52
+ service install [--domain D] systemd (Linux) / launchd (macOS) / schtasks (Windows)
53
+ service uninstall | logs [-f] | status
57
54
 
58
- schedule list [--json] List scheduled tasks
59
- upgrade [--dry-run] Self-upgrade from npm + restart service
55
+ Other:
56
+ schedule list [--json]
57
+ scopes List available tool groups
60
58
 
61
59
  Options:
62
60
  -h, --help Show this help
63
- --json Machine-readable output (where supported)
61
+ --json Machine-readable output where supported
64
62
  `;
65
63
  function parseArgs(argv) {
66
64
  const [command = '', ...rest] = argv;
67
65
  const sub = [];
68
66
  const flags = new Set();
69
67
  const values = new Map();
68
+ // Flags that always take a value (so `--paths /a /b` still works positionally)
69
+ const valueFlags = new Set(['name', 'paths', 'deny', 'scopes', 'rpm', 'expires', 'token', 'host', 'port', 'domain', 'tool', 'since', 'limit']);
70
70
  for (let i = 0; i < rest.length; i++) {
71
71
  const a = rest[i];
72
72
  if (a.startsWith('--')) {
73
73
  const key = a.slice(2);
74
+ const eq = key.indexOf('=');
75
+ if (eq > 0) {
76
+ values.set(key.slice(0, eq), key.slice(eq + 1));
77
+ continue;
78
+ }
74
79
  const next = rest[i + 1];
75
- if (next !== undefined && !next.startsWith('-')) {
80
+ if (valueFlags.has(key) && next !== undefined && !next.startsWith('--')) {
76
81
  values.set(key, next);
77
82
  i++;
78
83
  }
@@ -80,7 +85,7 @@ function parseArgs(argv) {
80
85
  flags.add(key);
81
86
  }
82
87
  }
83
- else if (a.startsWith('-') && a.length > 1) {
88
+ else if (a.startsWith('-') && a.length > 1 && !/^-\d/.test(a)) {
84
89
  for (const c of a.slice(1))
85
90
  flags.add(c);
86
91
  }
@@ -91,8 +96,9 @@ function parseArgs(argv) {
91
96
  return { command, sub, flags, values };
92
97
  }
93
98
  const jsonOut = (args) => args.flags.has('json');
99
+ const splitList = (s) => (s || '').split(',').map(x => x.trim()).filter(Boolean);
94
100
  // ---------------------------------------------------------------------------
95
- // token lookup helper
101
+ // helpers
96
102
  // ---------------------------------------------------------------------------
97
103
  function findToken(cfg, idOrName) {
98
104
  if (!idOrName)
@@ -104,8 +110,23 @@ function findToken(cfg, idOrName) {
104
110
  }
105
111
  return t;
106
112
  }
107
- function maskToken(t) {
108
- return t.slice(0, 6) + '…' + t.slice(-4);
113
+ const mask = (t) => t.slice(0, 6) + '…' + t.slice(-4);
114
+ function run(cmd, args) {
115
+ try {
116
+ return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
117
+ }
118
+ catch {
119
+ return '';
120
+ }
121
+ }
122
+ function banner() {
123
+ return `
124
+ ___ _____
125
+ | _ \\__ _ __ _ ___ _ _ |_ _|__ _ _ _ __ _ ___
126
+ | / _\` / _\` / -_) '_| | |/ - \\ '_| '_/ _\` / -_)
127
+ |_|_\\__,_\\__, \\___|_| |_|\\___/_| |_| \\__,_\\___|
128
+ |___/ ${PKG.name} v${PKG.version}
129
+ `;
109
130
  }
110
131
  // ---------------------------------------------------------------------------
111
132
  // init
@@ -113,9 +134,20 @@ function maskToken(t) {
113
134
  function cmdInit(args) {
114
135
  const existing = fs.existsSync(configPath());
115
136
  const cfg = existing ? loadConfig() : null;
137
+ const extraPaths = [...splitList(args.values.get('paths')), ...args.sub].map(resolveReal);
116
138
  if (cfg && cfg.tokens.length) {
117
- console.log('Config already exists with ' + cfg.tokens.length + ' token(s) — nothing to do.');
118
- console.log('Add more tokens with: ramcp token add --name <name>');
139
+ if (extraPaths.length) {
140
+ const t = primaryToken(cfg);
141
+ for (const p of extraPaths)
142
+ if (!t.allowed_paths.includes(p))
143
+ t.allowed_paths.push(p);
144
+ saveConfig(cfg);
145
+ console.log(`Added ${extraPaths.length} path(s) to token "${t.name}".`);
146
+ return;
147
+ }
148
+ console.log(`Config already exists with ${cfg.tokens.length} token(s) — nothing to do.`);
149
+ console.log('Add paths: ramcp policy allow <dir>');
150
+ console.log('Add tokens: ramcp token add --name <name>');
119
151
  return;
120
152
  }
121
153
  const fresh = cfg || {
@@ -128,29 +160,24 @@ function cmdInit(args) {
128
160
  read_only: false,
129
161
  tokens: [],
130
162
  };
131
- const rec = newTokenRecord({ name: 'default' });
163
+ const rec = newTokenRecord({ name: 'default', allowed_paths: extraPaths });
132
164
  fresh.tokens.push(rec);
133
165
  saveConfig(fresh);
166
+ console.log(banner());
167
+ console.log(`platform: ${platformLabel()}`);
168
+ console.log(`config: ${configPath()}`);
169
+ console.log(`token: ${mask(rec.token)} (full: ramcp token show --full)`);
170
+ console.log(`paths: ${extraPaths.length ? extraPaths.join(', ') : '(none — add with `ramcp policy allow <dir>`)'}`);
134
171
  console.log(`
135
- ___ _____
136
- | _ \\__ _ __ _ ___ _ _ |_ _|__ _ _ _ __ _ ___
137
- | / _\` / _\` / -_) '_| | |/ - \\ '_| '_/ _\` / -_)
138
- |_|_\\__,_\\__, \\___|_| |_|\\___/_| |_| \\__,_\\___|
139
- |___/ ${PKG.name} v${PKG.version}
140
-
141
- ${existing ? '✔ Existing config migrated to v2' : '✔ Config written to ' + configPath()}
142
- ✔ Token "default": ${maskToken(rec.token)} (full: ramcp token show default --full)
143
- ✔ Audit log: ${fresh.audit.enabled ? 'enabled' : 'disabled'} (${fresh.audit.db_path})
144
-
145
172
  Next steps:
146
- 1. ramcp start # run in foreground
147
- 2. ramcp service install # systemd + nginx (production)
148
- 3. ramcp policy allow /srv/myapp # let the AI touch your project
149
- 4. ramcp url # connector URL for your chatbot
173
+ 1. ramcp policy allow ~/projects # what the AI may read/write
174
+ 2. ramcp policy shell on # optional: allow commands
175
+ 3. ramcp tunnel # public https URL (laptops, no port forward)
176
+ or: ramcp service install # autostart on a server with a domain
150
177
  `);
151
178
  }
152
179
  // ---------------------------------------------------------------------------
153
- // start
180
+ // start / tunnel
154
181
  // ---------------------------------------------------------------------------
155
182
  async function cmdStart(args) {
156
183
  const { runServer } = await import('../server/run.js');
@@ -158,6 +185,16 @@ async function cmdStart(args) {
158
185
  host: args.values.get('host'),
159
186
  port: args.values.has('port') ? parseInt(args.values.get('port'), 10) : undefined,
160
187
  readOnly: args.flags.has('read-only'),
188
+ tunnel: args.flags.has('tunnel'),
189
+ });
190
+ }
191
+ async function cmdTunnel(args) {
192
+ const { runServer } = await import('../server/run.js');
193
+ await runServer({
194
+ host: args.values.get('host'),
195
+ port: args.values.has('port') ? parseInt(args.values.get('port'), 10) : undefined,
196
+ readOnly: args.flags.has('read-only'),
197
+ tunnel: true,
161
198
  });
162
199
  }
163
200
  // ---------------------------------------------------------------------------
@@ -165,33 +202,47 @@ async function cmdStart(args) {
165
202
  // ---------------------------------------------------------------------------
166
203
  function cmdUrl(args) {
167
204
  const cfg = loadConfig();
168
- const t = findToken(cfg, args.sub[0]);
205
+ const t = findToken(cfg, args.sub[0] || args.values.get('token'));
169
206
  const host = cfg.public_host || `${cfg.host}:${cfg.port}`;
170
207
  const scheme = cfg.public_host ? 'https' : 'http';
171
208
  console.log(`${scheme}://${host}/${t.token}${cfg.mcp_path}`);
209
+ if (!cfg.public_host) {
210
+ console.log('\n(local URL — run `ramcp tunnel` for a public https link)');
211
+ }
172
212
  }
173
213
  // ---------------------------------------------------------------------------
174
- // token management
214
+ // token
175
215
  // ---------------------------------------------------------------------------
176
216
  function cmdToken(args) {
177
217
  const cfg = loadConfig();
178
218
  const sub = args.sub[0];
179
- if (sub === 'list') {
219
+ if (sub === 'list' || !sub) {
180
220
  if (jsonOut(args)) {
181
221
  console.log(JSON.stringify(cfg.tokens.map(t => ({
182
222
  id: t.id, name: t.name, created: t.created, expires: t.expires || null,
183
223
  scopes: t.scopes, read_only: !!t.read_only, shell: t.shell_enabled,
224
+ allowed_paths: t.allowed_paths, denied_paths: t.denied_paths,
225
+ rpm: t.max_requests_per_minute || null,
184
226
  fingerprint: AuditLog.fingerprint(t.token),
185
227
  })), null, 2));
186
228
  return;
187
229
  }
188
230
  for (const t of cfg.tokens) {
189
- console.log(`${t.id} ${t.name.padEnd(16)} shell:${t.shell_enabled ? 'on ' : 'off'} ro:${t.read_only ? 'yes' : 'no '} ${t.expires ? 'expires ' + t.expires + ' ' : ''}${t.scopes.length ? 'scopes: ' + t.scopes.join(',') + ' ' : ''}fp:${AuditLog.fingerprint(t.token)}`);
231
+ const bits = [
232
+ t.id,
233
+ t.name.padEnd(16),
234
+ `shell:${t.shell_enabled ? 'on ' : 'off'}`,
235
+ `ro:${t.read_only ? 'yes' : 'no '}`,
236
+ `paths:${t.allowed_paths.length}`,
237
+ t.expires ? `expires:${t.expires.slice(0, 10)}` : '',
238
+ t.scopes.length ? `scopes:${t.scopes.join('|')}` : '',
239
+ ].filter(Boolean);
240
+ console.log(bits.join(' '));
190
241
  }
191
242
  return;
192
243
  }
193
244
  if (sub === 'add') {
194
- const name = args.values.get('name');
245
+ const name = args.values.get('name') || args.sub[1];
195
246
  if (!name) {
196
247
  console.error('--name is required');
197
248
  process.exit(1);
@@ -203,107 +254,145 @@ function cmdToken(args) {
203
254
  const rec = newTokenRecord({
204
255
  name,
205
256
  shell_enabled: args.flags.has('shell') ? true : args.flags.has('no-shell') ? false : undefined,
206
- allowed_paths: args.values.get('paths')?.split(',').map(s => s.trim()).filter(Boolean),
207
- denied_paths: args.values.get('deny')?.split(',').map(s => s.trim()).filter(Boolean),
208
- scopes: args.values.get('scopes')?.split(',').map(s => s.trim()).filter(Boolean),
257
+ allowed_paths: splitList(args.values.get('paths')).map(resolveReal),
258
+ denied_paths: splitList(args.values.get('deny')).map(resolveReal),
259
+ scopes: splitList(args.values.get('scopes')),
209
260
  read_only: args.flags.has('read-only'),
210
261
  max_requests_per_minute: args.values.has('rpm') ? parseInt(args.values.get('rpm'), 10) : undefined,
211
262
  expires: args.values.get('expires'),
212
263
  });
213
- // resolve allowed paths to real paths
214
- rec.allowed_paths = rec.allowed_paths.map(resolveReal);
215
- rec.denied_paths = rec.denied_paths.map(resolveReal);
216
264
  cfg.tokens.push(rec);
217
265
  saveConfig(cfg);
266
+ const host = cfg.public_host || `${cfg.host}:${cfg.port}`;
267
+ const scheme = cfg.public_host ? 'https' : 'http';
218
268
  console.log(`Token "${name}" created (${rec.id})`);
219
- console.log(`URL: https://${cfg.public_host || cfg.host + ':' + cfg.port}/${rec.token}${cfg.mcp_path}`);
220
- restartHint();
269
+ console.log(`URL: ${scheme}://${host}/${rec.token}${cfg.mcp_path}`);
221
270
  return;
222
271
  }
223
272
  if (sub === 'show') {
224
- const t = findToken(cfg, args.sub[1]);
225
- if (args.flags.has('full'))
226
- console.log(t.token);
227
- else
228
- console.log(`${t.name}: ${maskToken(t.token)} (use --full for the complete token)`);
273
+ const t = findToken(cfg, args.sub[1] || args.values.get('token'));
274
+ console.log(args.flags.has('full') ? t.token : `${t.name}: ${mask(t.token)} (--full for the complete token)`);
229
275
  return;
230
276
  }
231
277
  if (sub === 'rotate') {
232
- const t = findToken(cfg, args.sub[1]);
278
+ const t = findToken(cfg, args.sub[1] || args.values.get('token'));
233
279
  t.token = generateToken();
234
280
  saveConfig(cfg);
235
281
  console.log(`Token "${t.name}" rotated: ${t.token}`);
236
- console.log('Old token is dead. Update your chatbot connector now.');
237
- restartHint();
282
+ console.log('Old token is dead. Update your chatbot connector.');
238
283
  return;
239
284
  }
240
285
  if (sub === 'revoke') {
241
- const t = findToken(cfg, args.sub[1]);
242
- cfg.tokens = cfg.tokens.filter(x => x !== t);
243
- if (!cfg.tokens.length) {
286
+ const t = findToken(cfg, args.sub[1] || args.values.get('token'));
287
+ if (cfg.tokens.length === 1) {
244
288
  console.error('Cannot revoke the last token.');
245
289
  process.exit(1);
246
290
  }
291
+ cfg.tokens = cfg.tokens.filter(x => x !== t);
247
292
  saveConfig(cfg);
248
293
  console.log(`Token "${t.name}" (${t.id}) revoked.`);
249
- restartHint();
250
294
  return;
251
295
  }
252
296
  console.log('Usage: ramcp token <list|add|show|rotate|revoke> …');
253
297
  process.exit(1);
254
298
  }
255
299
  // ---------------------------------------------------------------------------
256
- // policy
300
+ // policy — multi-path, token-selectable
257
301
  // ---------------------------------------------------------------------------
258
302
  function cmdPolicy(args) {
259
303
  const cfg = loadConfig();
260
- // global readonly toggle has no token selector
261
- if (args.sub[0] === 'readonly' && (args.sub[1] === 'on' || args.sub[1] === 'off')) {
262
- cfg.read_only = args.sub[1] === 'on';
304
+ const action = args.sub[0] || 'show';
305
+ // Global switch (no token selector)
306
+ if (action === 'readonly') {
307
+ const val = args.sub[1];
308
+ if (val !== 'on' && val !== 'off') {
309
+ console.error('Usage: ramcp policy readonly on|off');
310
+ process.exit(1);
311
+ }
312
+ cfg.read_only = val === 'on';
263
313
  saveConfig(cfg);
264
- console.log(`Global read-only: ${cfg.read_only ? 'ON — mutating tools refused' : 'off'}`);
265
- restartHint();
314
+ console.log(`Global read-only: ${cfg.read_only ? 'ON — all mutating tools refused' : 'off'}`);
266
315
  return;
267
316
  }
268
- const t = findToken(cfg, args.sub[0] === 'allow' || args.sub[0] === 'deny' || args.sub[0] === 'shell' ? undefined : args.sub[0]);
269
- const sub = args.sub[0] === 'allow' || args.sub[0] === 'deny' || args.sub[0] === 'shell' ? args.sub[0] : (args.sub[1] || 'show');
270
- if (sub === 'allow' && args.values.size === 0 && !args.sub.find(s => s.startsWith('/'))) {
271
- // ramcp policy allow <path> — path is in sub[1] (or sub[2] when selector given)
272
- }
273
- const pathArg = (() => {
274
- const idx = args.sub.indexOf('allow') >= 0 ? args.sub.indexOf('allow') : args.sub.indexOf('deny');
275
- return idx >= 0 ? args.sub[idx + 1] : undefined;
276
- })();
277
- if (sub === 'allow' && pathArg) {
278
- const real = resolveReal(pathArg);
279
- if (!t.allowed_paths.includes(real))
280
- t.allowed_paths.push(real);
317
+ const t = findToken(cfg, args.values.get('token'));
318
+ const rest = args.sub.slice(1);
319
+ if (action === 'allow' || action === 'deny') {
320
+ const targets = [...rest, ...splitList(args.values.get('paths'))].map(resolveReal);
321
+ if (!targets.length) {
322
+ console.error(`Usage: ramcp policy ${action} <path> [path...] [--token N]`);
323
+ process.exit(1);
324
+ }
325
+ for (const p of targets) {
326
+ if (action === 'allow') {
327
+ t.denied_paths = t.denied_paths.filter(d => d !== p);
328
+ if (!t.allowed_paths.includes(p))
329
+ t.allowed_paths.push(p);
330
+ }
331
+ else {
332
+ t.allowed_paths = t.allowed_paths.filter(a => a !== p);
333
+ if (!t.denied_paths.includes(p))
334
+ t.denied_paths.push(p);
335
+ }
336
+ }
281
337
  saveConfig(cfg);
282
- console.log(`Allowed: ${real}`);
283
- restartHint();
284
- }
285
- else if (sub === 'deny' && pathArg) {
286
- const real = resolveReal(pathArg);
287
- t.allowed_paths = t.allowed_paths.filter((p) => p !== real);
288
- if (!t.denied_paths.includes(real))
289
- t.denied_paths.push(real);
338
+ console.log(`${action === 'allow' ? 'Allowed' : 'Denied'} for "${t.name}":`);
339
+ for (const p of targets)
340
+ console.log(` ${p}`);
341
+ return;
342
+ }
343
+ if (action === 'shell') {
344
+ const val = rest[0];
345
+ if (val !== 'on' && val !== 'off') {
346
+ console.error('Usage: ramcp policy shell on|off [--token N]');
347
+ process.exit(1);
348
+ }
349
+ t.shell_enabled = val === 'on';
290
350
  saveConfig(cfg);
291
- console.log(`Denied: ${real}`);
292
- restartHint();
351
+ console.log(`Shell for "${t.name}": ${t.shell_enabled ? 'enabled' : 'disabled'}`);
352
+ return;
293
353
  }
294
- else if (sub === 'shell' && (args.sub.includes('on') || args.sub.includes('off'))) {
295
- t.shell_enabled = args.sub.includes('on');
354
+ if (action === 'scopes') {
355
+ const raw = rest.join(',') || args.values.get('scopes') || '';
356
+ if (!raw) {
357
+ console.error('Usage: ramcp policy scopes <group,group|all> [--token N]');
358
+ process.exit(1);
359
+ }
360
+ if (raw === 'all') {
361
+ t.scopes = [];
362
+ }
363
+ else {
364
+ const groups = splitList(raw);
365
+ const unknown = groups.filter(g => !(g in TOOL_SCOPES));
366
+ if (unknown.length) {
367
+ console.error(`Unknown scope group(s): ${unknown.join(', ')}`);
368
+ console.error(`Available: ${Object.keys(TOOL_SCOPES).join(', ')}`);
369
+ process.exit(1);
370
+ }
371
+ t.scopes = groups;
372
+ }
296
373
  saveConfig(cfg);
297
- console.log(`Shell execution for "${t.name}": ${t.shell_enabled ? 'enabled' : 'disabled'}`);
298
- restartHint();
374
+ console.log(`Scopes for "${t.name}": ${t.scopes.length ? t.scopes.join(', ') : '(all tools)'}`);
375
+ return;
299
376
  }
300
- else {
301
- console.log(`token: ${t.name} (${t.id})`);
302
- console.log(`allowed: ${t.allowed_paths.length ? '\n ' + t.allowed_paths.join('\n ') : '(none)'}`);
303
- console.log(`denied: ${t.denied_paths.length ? '\n ' + t.denied_paths.join('\n ') : '(none)'}`);
304
- console.log(`shell: ${t.shell_enabled ? 'enabled' : 'disabled'}`);
305
- console.log(`read_only: ${t.read_only ? 'yes' : 'no'}`);
306
- console.log(`scopes: ${t.scopes.length ? t.scopes.join(', ') : '(all tools)'}`);
377
+ // show
378
+ if (jsonOut(args)) {
379
+ console.log(JSON.stringify({
380
+ token: t.name, allowed_paths: t.allowed_paths, denied_paths: t.denied_paths,
381
+ shell: t.shell_enabled, read_only: !!t.read_only, scopes: t.scopes,
382
+ global_read_only: cfg.read_only,
383
+ }, null, 2));
384
+ return;
385
+ }
386
+ console.log(`token: ${t.name} (${t.id})`);
387
+ console.log(`allowed: ${t.allowed_paths.length ? '\n ' + t.allowed_paths.join('\n ') : '(none — the AI can read nothing)'}`);
388
+ console.log(`denied: ${t.denied_paths.length ? '\n ' + t.denied_paths.join('\n ') : '(none)'}`);
389
+ console.log(`shell: ${t.shell_enabled ? 'enabled' : 'disabled'}`);
390
+ console.log(`read_only: ${t.read_only ? 'yes' : 'no'}${cfg.read_only ? ' (global read-only is ON)' : ''}`);
391
+ console.log(`scopes: ${t.scopes.length ? t.scopes.join(', ') : '(all tools)'}`);
392
+ }
393
+ function cmdScopes() {
394
+ for (const [group, tools] of Object.entries(TOOL_SCOPES)) {
395
+ console.log(`${group.padEnd(12)} ${tools.join(', ')}`);
307
396
  }
308
397
  }
309
398
  // ---------------------------------------------------------------------------
@@ -312,14 +401,14 @@ function cmdPolicy(args) {
312
401
  function cmdAudit(args) {
313
402
  const cfg = loadConfig();
314
403
  if (!cfg.audit.enabled) {
315
- console.log('Audit log is disabled (audit.enabled = false).');
404
+ console.log('Audit log is disabled.');
316
405
  return;
317
406
  }
318
407
  const audit = new AuditLog(cfg.audit.db_path);
319
408
  if (args.sub[0] === 'chain' || args.flags.has('verify')) {
320
- const tampered = audit.verify();
321
- console.log(tampered === null ? '✔ hash chain intact' : `✖ TAMPERED — first bad row id: ${tampered}`);
322
- process.exit(tampered === null ? 0 : 2);
409
+ const bad = audit.verify();
410
+ console.log(bad === null ? '✔ hash chain intact' : `✖ TAMPERED — first bad entry: line ${bad}`);
411
+ process.exit(bad === null ? 0 : 2);
323
412
  }
324
413
  const rows = audit.query({
325
414
  tool: args.values.get('tool'),
@@ -328,181 +417,278 @@ function cmdAudit(args) {
328
417
  });
329
418
  if (jsonOut(args)) {
330
419
  console.log(JSON.stringify(rows, null, 2));
420
+ return;
331
421
  }
332
- else {
333
- if (!rows.length) {
334
- console.log('(no audit entries)');
335
- return;
336
- }
337
- for (const r of rows) {
338
- const d = new Date(r.ts).toISOString().slice(0, 19);
339
- console.log(`${d} fp:${r.token_fingerprint} ${r.tool.padEnd(20)} ${r.is_error ? 'ERR ' : 'ok '} ${r.duration_ms}ms`);
340
- }
422
+ if (!rows.length) {
423
+ console.log('(no audit entries)');
424
+ return;
425
+ }
426
+ for (const r of rows) {
427
+ console.log(`${new Date(r.ts).toISOString().slice(0, 19)} fp:${r.token_fingerprint} ${r.tool.padEnd(22)} ${r.is_error ? 'ERR' : 'ok '} ${r.duration_ms}ms`);
341
428
  }
342
- audit.close();
343
429
  }
344
430
  // ---------------------------------------------------------------------------
345
431
  // doctor
346
432
  // ---------------------------------------------------------------------------
347
433
  async function cmdDoctor(args) {
348
434
  const results = [];
349
- const ok = (name, msg) => results.push([`✔ ${name}`, msg]);
350
- const bad = (name, msg) => results.push([`✖ ${name}`, msg]);
351
- const warn = (name, msg) => results.push([`⚠ ${name}`, msg]);
435
+ const ok = (n, m) => results.push([`✔ ${n}`, m]);
436
+ const bad = (n, m) => results.push([`✖ ${n}`, m]);
437
+ const warn = (n, m) => results.push([`⚠ ${n}`, m]);
352
438
  const cfg = loadConfig();
353
- // 1. token
354
- if (cfg.tokens.length)
355
- ok('tokens', `${cfg.tokens.length} configured, primary: ${cfg.tokens[0].name}`);
356
- else
439
+ ok('platform', platformLabel());
440
+ ok('config', configPath());
441
+ if (cfg.tokens.length) {
442
+ const withPaths = cfg.tokens.filter(t => t.allowed_paths.length).length;
443
+ ok('tokens', `${cfg.tokens.length} configured, ${withPaths} with path access`);
444
+ if (!withPaths)
445
+ warn('paths', 'no token has any allowed path — add with `ramcp policy allow <dir>`');
446
+ }
447
+ else {
357
448
  bad('tokens', 'none — run `ramcp init`');
358
- // 2. bind port free?
449
+ }
450
+ // port / gateway
359
451
  const inUse = await new Promise((resolve) => {
360
452
  const srv = net.createServer();
361
453
  srv.once('error', () => resolve(true));
362
- srv.once('listening', () => { srv.close(() => resolve(false)); });
454
+ srv.once('listening', () => srv.close(() => resolve(false)));
363
455
  srv.listen(cfg.port, cfg.host);
364
456
  });
365
- const serviceActive = fs.existsSync(SERVICE_FILE) && run('systemctl', ['is-active', SERVICE_NAME]).trim() === 'active';
457
+ const svcActive = serviceIsActive();
366
458
  if (inUse) {
367
- serviceActive ? ok('port', `${cfg.host}:${cfg.port} in use by the running service`) : warn('port', `${cfg.host}:${cfg.port} in use by ANOTHER process`);
368
- }
369
- else {
370
- serviceActive ? warn('port', `port free but service claims active — crashed?`) : warn('port', `not listening — start with \`ramcp service install\` or \`ramcp start\``);
371
- }
372
- // 3. gateway answers?
373
- if (!inUse || serviceActive) {
374
459
  try {
375
- const resp = await fetch(`http://${cfg.host}:${cfg.port}/health`);
460
+ const resp = await fetch(`http://${cfg.host}:${cfg.port}/health`, { signal: AbortSignal.timeout(4000) });
376
461
  const body = (await resp.json());
377
- if (body.status === 'ok')
378
- ok('gateway', `healthy (v${body.version})`);
379
- else
380
- bad('gateway', `unexpected: ${JSON.stringify(body)}`);
462
+ body.status === 'ok'
463
+ ? ok('gateway', `healthy on ${cfg.host}:${cfg.port} (v${body.version})`)
464
+ : bad('gateway', `unexpected reply: ${JSON.stringify(body)}`);
381
465
  }
382
466
  catch {
383
- bad('gateway', `no answer on ${cfg.host}:${cfg.port}/health`);
467
+ warn('gateway', `port ${cfg.port} is in use but /health did not answer — another app?`);
384
468
  }
385
469
  }
386
- // 4. nginx vhost?
470
+ else {
471
+ warn('gateway', `not running — start with \`ramcp tunnel\` or \`ramcp start\``);
472
+ }
473
+ if (svcActive)
474
+ ok('service', `${serviceLabel()} active`);
475
+ else if (serviceIsInstalled())
476
+ warn('service', `${serviceLabel()} installed but not active`);
477
+ // tunnel readiness
478
+ const cf = resolveCloudflared();
479
+ cf ? ok('tunnel', `cloudflared ready (${cf})`) : warn('tunnel', 'cloudflared not installed yet — `ramcp tunnel` downloads it automatically');
480
+ // public reachability
387
481
  if (cfg.public_host) {
388
- const vhost = `/etc/nginx/sites-enabled/${cfg.public_host}`;
389
- if (fs.existsSync(vhost)) {
390
- const conf = fs.readFileSync(vhost, 'utf8');
391
- if (conf.includes(`proxy_pass http://${cfg.host}:${cfg.port}`)) {
392
- ok('nginx', `vhost for ${cfg.public_host} → ${cfg.host}:${cfg.port}`);
393
- }
394
- else {
395
- warn('nginx', `${vhost} exists but does not proxy to ${cfg.host}:${cfg.port}`);
396
- }
397
- }
398
- else {
399
- warn('nginx', `no vhost at ${vhost} — run \`ramcp service install --domain ${cfg.public_host}\``);
400
- }
401
- // 5. public DNS resolves + Cloudflare edge reachable?
402
482
  try {
403
483
  const resp = await fetch(`https://${cfg.public_host}/health`, { signal: AbortSignal.timeout(8000) });
404
484
  const body = (await resp.json());
405
- if (body.status === 'ok')
406
- ok('public', `https://${cfg.public_host} healthy end-to-end`);
407
- else
408
- warn('public', `got status ${resp.status} from edge`);
485
+ body.status === 'ok' ? ok('public', `https://${cfg.public_host} reachable`) : warn('public', `edge answered ${resp.status}`);
409
486
  }
410
487
  catch (e) {
411
- warn('public', `https://${cfg.public_host} unreachable: ${e.message}`);
488
+ warn('public', `https://${cfg.public_host} unreachable (${e.message})`);
412
489
  }
413
490
  }
414
- // 6. audit chain
415
- if (cfg.audit.enabled && fs.existsSync(cfg.audit.db_path.replace(/\.db$/, '') + '.jsonl')) {
416
- const audit = new AuditLog(cfg.audit.db_path);
417
- const tampered = audit.verify();
418
- tampered === null ? ok('audit', 'hash chain intact') : bad('audit', `tampered at row ${tampered}`);
419
- audit.close();
420
- }
421
- else if (cfg.audit.enabled) {
422
- warn('audit', 'enabled but db not created yet');
423
- }
424
- for (const [name, msg] of results)
425
- console.log(`${name.padEnd(14)} ${msg}`);
426
- const hasBad = results.some(r => r[0].startsWith('✖'));
427
- process.exitCode = hasBad ? 1 : 0;
428
- }
429
- function run(cmd, args) {
430
- try {
431
- return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
432
- }
433
- catch {
434
- return '';
491
+ // audit
492
+ if (cfg.audit.enabled) {
493
+ const file = cfg.audit.db_path.replace(/\.db$/, '') + (cfg.audit.db_path.endsWith('.jsonl') ? '' : '.jsonl');
494
+ if (fs.existsSync(file) || fs.existsSync(cfg.audit.db_path)) {
495
+ const a = new AuditLog(cfg.audit.db_path);
496
+ const t = a.verify();
497
+ t === null ? ok('audit', 'hash chain intact') : bad('audit', `tampered at line ${t}`);
498
+ }
499
+ else {
500
+ warn('audit', 'enabled, no entries yet');
501
+ }
435
502
  }
503
+ for (const [n, m] of results)
504
+ console.log(`${n.padEnd(14)} ${m}`);
505
+ process.exitCode = results.some(r => r[0].startsWith('✖')) ? 1 : 0;
436
506
  }
437
507
  // ---------------------------------------------------------------------------
438
- // service
508
+ // service — systemd / launchd / schtasks
439
509
  // ---------------------------------------------------------------------------
440
510
  const SERVICE_NAME = 'remote-access-mcp';
441
- const SERVICE_FILE = `/etc/systemd/system/${SERVICE_NAME}.service`;
442
- function isSystemdInstalled() {
443
- return fs.existsSync(SERVICE_FILE);
511
+ const SYSTEMD_FILE = `/etc/systemd/system/${SERVICE_NAME}.service`;
512
+ const LAUNCHD_LABEL = 'ir.amiralimanzar.ramcp';
513
+ const launchdFile = () => path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
514
+ const SCHTASK_NAME = 'RemoteAccessMCP';
515
+ function serviceLabel() {
516
+ if (hasSystemd())
517
+ return 'systemd unit';
518
+ if (isMac())
519
+ return 'launchd agent';
520
+ if (isWindows())
521
+ return 'scheduled task';
522
+ return 'service';
444
523
  }
445
- function restartHint() {
446
- if (isSystemdInstalled()) {
447
- console.log('(service restarts automatically on next request — config hot-reloads)');
448
- }
524
+ function serviceIsInstalled() {
525
+ if (hasSystemd())
526
+ return fs.existsSync(SYSTEMD_FILE);
527
+ if (isMac())
528
+ return fs.existsSync(launchdFile());
529
+ if (isWindows())
530
+ return run('schtasks', ['/query', '/tn', SCHTASK_NAME]).length > 0;
531
+ return false;
532
+ }
533
+ function serviceIsActive() {
534
+ if (hasSystemd())
535
+ return run('systemctl', ['is-active', SERVICE_NAME]).trim() === 'active';
536
+ if (isMac())
537
+ return run('launchctl', ['list']).includes(LAUNCHD_LABEL);
538
+ if (isWindows())
539
+ return /Running/i.test(run('schtasks', ['/query', '/tn', SCHTASK_NAME, '/fo', 'LIST', '/v']));
540
+ return false;
449
541
  }
450
542
  function cmdService(args) {
451
543
  const sub = args.sub[0] || '';
452
- if (sub === 'install') {
453
- installService(args);
454
- }
455
- else if (sub === 'uninstall') {
456
- uninstallService();
457
- }
458
- else if (sub === 'logs') {
459
- const flags = args.flags.has('f') || args.flags.has('follow') ? ['-f'] : [];
460
- try {
461
- execFileSync('journalctl', ['-u', SERVICE_NAME, '-n', '100', ...flags], { stdio: 'inherit' });
544
+ if (sub === 'install')
545
+ return installService(args);
546
+ if (sub === 'uninstall')
547
+ return uninstallService();
548
+ if (sub === 'status')
549
+ return cmdStatus();
550
+ if (sub === 'logs') {
551
+ if (hasSystemd()) {
552
+ const f = args.flags.has('f') || args.flags.has('follow') ? ['-f'] : [];
553
+ try {
554
+ execFileSync('journalctl', ['-u', SERVICE_NAME, '-n', '100', ...f], { stdio: 'inherit' });
555
+ }
556
+ catch (e) {
557
+ console.error(e.message);
558
+ process.exit(1);
559
+ }
560
+ return;
462
561
  }
463
- catch (e) {
464
- console.error(e.message);
465
- process.exit(1);
562
+ const log = path.join(configDir(), 'gateway.log');
563
+ if (!fs.existsSync(log)) {
564
+ console.log(`No log file at ${log}`);
565
+ return;
466
566
  }
567
+ if (args.flags.has('f') || args.flags.has('follow')) {
568
+ console.log(`(following ${log} — Ctrl+C to stop)`);
569
+ let size = fs.statSync(log).size;
570
+ setInterval(() => {
571
+ const s = fs.statSync(log).size;
572
+ if (s > size) {
573
+ const fd = fs.openSync(log, 'r');
574
+ const buf = Buffer.alloc(s - size);
575
+ fs.readSync(fd, buf, 0, buf.length, size);
576
+ fs.closeSync(fd);
577
+ process.stdout.write(buf.toString());
578
+ size = s;
579
+ }
580
+ }, 700);
581
+ return;
582
+ }
583
+ const content = fs.readFileSync(log, 'utf8').split('\n').slice(-100).join('\n');
584
+ console.log(content);
585
+ return;
467
586
  }
468
- else if (sub === 'status') {
469
- cmdStatus();
470
- }
471
- else {
472
- console.log('Usage: ramcp service <install [--domain D]|uninstall|logs [-f]|status>');
473
- process.exit(1);
474
- }
587
+ console.log('Usage: ramcp service <install [--domain D]|uninstall|logs [-f]|status>');
588
+ process.exit(1);
475
589
  }
476
590
  function installService(args) {
477
- if (process.getuid?.() !== 0) {
478
- console.error('service install requires root (sudo).');
479
- process.exit(1);
480
- }
481
- const domain = args.values.get('domain') || '';
482
591
  const cfg = loadConfig();
483
592
  const nodeBin = process.execPath;
484
- const pkgRoot = PKG_ROOT;
485
- const unit = `[Unit]
593
+ const entry = path.join(PKG_ROOT, 'dist', 'server', 'run.js');
594
+ const dryRun = args.flags.has('dry-run');
595
+ const wantTunnel = args.flags.has('tunnel');
596
+ const domain = args.values.get('domain') || '';
597
+ // ---- Linux (systemd) ----
598
+ if (hasSystemd()) {
599
+ if (process.getuid?.() !== 0) {
600
+ console.error('systemd install requires root (sudo).');
601
+ process.exit(1);
602
+ }
603
+ const unit = `[Unit]
486
604
  Description=Remote Access MCP Gateway
487
605
  After=network.target
488
606
 
489
607
  [Service]
490
608
  Type=simple
491
- ExecStart=${nodeBin} ${pkgRoot}/dist/server/run.js
609
+ ExecStart=${nodeBin} ${entry}${wantTunnel ? ' --tunnel' : ''}
492
610
  Restart=on-failure
493
611
  RestartSec=5
494
612
  Environment=NODE_ENV=production
613
+ Environment=HOME=${os.homedir()}
495
614
 
496
615
  [Install]
497
616
  WantedBy=multi-user.target
498
617
  `;
499
- fs.writeFileSync(SERVICE_FILE, unit);
500
- execFileSync('systemctl', ['daemon-reload']);
501
- execFileSync('systemctl', ['enable', '--now', SERVICE_NAME]);
502
- console.log(`✔ systemd service installed and started: ${SERVICE_NAME}`);
503
- if (domain) {
504
- const nginxFile = `/etc/nginx/sites-available/${domain}`;
505
- const vhost = `server {
618
+ if (dryRun) {
619
+ console.log(`--- ${SYSTEMD_FILE} ---\n${unit}`);
620
+ return;
621
+ }
622
+ fs.writeFileSync(SYSTEMD_FILE, unit);
623
+ execFileSync('systemctl', ['daemon-reload']);
624
+ execFileSync('systemctl', ['enable', '--now', SERVICE_NAME]);
625
+ console.log(`✔ systemd service installed and started: ${SERVICE_NAME}`);
626
+ if (domain)
627
+ installNginx(domain, cfg, dryRun);
628
+ else
629
+ console.log('Tip: --domain mcp.example.com also writes an nginx vhost.');
630
+ return;
631
+ }
632
+ // ---- macOS (launchd, per-user, no sudo) ----
633
+ if (isMac()) {
634
+ const out = path.join(configDir(), 'gateway.log');
635
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
636
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
637
+ <plist version="1.0">
638
+ <dict>
639
+ <key>Label</key><string>${LAUNCHD_LABEL}</string>
640
+ <key>ProgramArguments</key>
641
+ <array>
642
+ <string>${nodeBin}</string>
643
+ <string>${entry}</string>${wantTunnel ? '\n <string>--tunnel</string>' : ''}
644
+ </array>
645
+ <key>RunAtLoad</key><true/>
646
+ <key>KeepAlive</key><true/>
647
+ <key>StandardOutPath</key><string>${out}</string>
648
+ <key>StandardErrorPath</key><string>${out}</string>
649
+ <key>EnvironmentVariables</key>
650
+ <dict><key>HOME</key><string>${os.homedir()}</string></dict>
651
+ </dict>
652
+ </plist>
653
+ `;
654
+ if (dryRun) {
655
+ console.log(`--- ${launchdFile()} ---\n${plist}`);
656
+ return;
657
+ }
658
+ fs.mkdirSync(path.dirname(launchdFile()), { recursive: true });
659
+ fs.writeFileSync(launchdFile(), plist);
660
+ run('launchctl', ['unload', launchdFile()]);
661
+ execFileSync('launchctl', ['load', '-w', launchdFile()]);
662
+ console.log(`✔ launchd agent installed: ${LAUNCHD_LABEL}`);
663
+ console.log(` logs: ${out}`);
664
+ return;
665
+ }
666
+ // ---- Windows (Scheduled Task at logon, no admin needed) ----
667
+ if (isWindows()) {
668
+ const log = path.join(configDir(), 'gateway.log');
669
+ const cmd = `"${nodeBin}" "${entry}"${wantTunnel ? ' --tunnel' : ''}`;
670
+ if (dryRun) {
671
+ console.log(`schtasks /create /tn ${SCHTASK_NAME} /tr ${cmd} /sc onlogon /rl limited /f`);
672
+ return;
673
+ }
674
+ try {
675
+ execFileSync('schtasks', ['/create', '/tn', SCHTASK_NAME, '/tr', cmd, '/sc', 'onlogon', '/rl', 'limited', '/f'], { stdio: 'inherit' });
676
+ execFileSync('schtasks', ['/run', '/tn', SCHTASK_NAME], { stdio: 'inherit' });
677
+ console.log(`✔ scheduled task installed and started: ${SCHTASK_NAME}`);
678
+ console.log(` logs: ${log}`);
679
+ }
680
+ catch (e) {
681
+ console.error(`schtasks failed: ${e.message}`);
682
+ process.exit(1);
683
+ }
684
+ return;
685
+ }
686
+ console.error(`No supported service manager on ${platform()}. Run \`ramcp start\` manually or use your own supervisor.`);
687
+ process.exit(1);
688
+ }
689
+ function installNginx(domain, cfg, dryRun) {
690
+ const nginxFile = `/etc/nginx/sites-available/${domain}`;
691
+ const vhost = `server {
506
692
  listen 80;
507
693
  listen [::]:80;
508
694
  server_name ${domain};
@@ -527,75 +713,149 @@ WantedBy=multi-user.target
527
713
  error_log /var/log/nginx/${domain}.error.log;
528
714
  }
529
715
  `;
530
- fs.writeFileSync(nginxFile, vhost);
531
- const link = `/etc/nginx/sites-enabled/${domain}`;
532
- if (!fs.existsSync(link))
533
- fs.symlinkSync(nginxFile, link);
534
- try {
535
- execFileSync('nginx', ['-t']);
536
- execFileSync('systemctl', ['reload', 'nginx']);
537
- console.log(`✔ nginx vhost installed: ${domain} → ${cfg.host}:${cfg.port}`);
538
- }
539
- catch (e) {
540
- console.error(`nginx config test failed:\n${e.stdout || e.message}`);
541
- process.exit(1);
542
- }
716
+ if (dryRun) {
717
+ console.log(`--- ${nginxFile} ---\n${vhost}`);
718
+ return;
543
719
  }
544
- else {
545
- console.log('Note: pass --domain mcp.example.com to also install the nginx vhost.');
720
+ fs.writeFileSync(nginxFile, vhost);
721
+ const link = `/etc/nginx/sites-enabled/${domain}`;
722
+ if (!fs.existsSync(link))
723
+ fs.symlinkSync(nginxFile, link);
724
+ try {
725
+ execFileSync('nginx', ['-t']);
726
+ execFileSync('systemctl', ['reload', 'nginx']);
727
+ console.log(`✔ nginx vhost installed: ${domain} → ${cfg.host}:${cfg.port}`);
546
728
  }
547
- }
548
- function uninstallService() {
549
- if (process.getuid?.() !== 0) {
550
- console.error('service uninstall requires root (sudo).');
729
+ catch (e) {
730
+ console.error(`nginx config test failed:\n${e.stdout || e.message}`);
551
731
  process.exit(1);
552
732
  }
553
- if (fs.existsSync(SERVICE_FILE)) {
554
- try {
555
- execFileSync('systemctl', ['disable', '--now', SERVICE_NAME]);
733
+ }
734
+ function uninstallService() {
735
+ if (hasSystemd()) {
736
+ if (process.getuid?.() !== 0) {
737
+ console.error('uninstall requires root (sudo).');
738
+ process.exit(1);
556
739
  }
557
- catch { /* not running */ }
558
- fs.unlinkSync(SERVICE_FILE);
559
- execFileSync('systemctl', ['daemon-reload']);
560
- console.log(`✔ systemd service removed`);
561
- }
562
- else {
563
- console.log('Service not installed.');
564
- }
565
- const sitesDir = '/etc/nginx/sites-enabled';
566
- if (fs.existsSync(sitesDir)) {
567
- const cfg = loadConfig();
568
- for (const f of fs.readdirSync(sitesDir)) {
569
- try {
570
- const content = fs.readFileSync(path.join(sitesDir, f), 'utf8');
571
- if (content.includes(`proxy_pass http://${cfg.host}:${cfg.port}`)) {
572
- fs.unlinkSync(path.join(sitesDir, f));
573
- console.log(`✔ nginx vhost removed: ${f}`);
740
+ if (fs.existsSync(SYSTEMD_FILE)) {
741
+ run('systemctl', ['disable', '--now', SERVICE_NAME]);
742
+ fs.unlinkSync(SYSTEMD_FILE);
743
+ execFileSync('systemctl', ['daemon-reload']);
744
+ console.log('✔ systemd service removed');
745
+ }
746
+ else
747
+ console.log('Service not installed.');
748
+ const sitesDir = '/etc/nginx/sites-enabled';
749
+ if (fs.existsSync(sitesDir)) {
750
+ const cfg = loadConfig();
751
+ for (const f of fs.readdirSync(sitesDir)) {
752
+ try {
753
+ if (fs.readFileSync(path.join(sitesDir, f), 'utf8').includes(`proxy_pass http://${cfg.host}:${cfg.port}`)) {
754
+ fs.unlinkSync(path.join(sitesDir, f));
755
+ console.log(`✔ nginx vhost removed: ${f}`);
756
+ }
574
757
  }
758
+ catch { /* skip */ }
575
759
  }
576
- catch { /* skip unreadable */ }
760
+ run('nginx', ['-t']) && run('systemctl', ['reload', 'nginx']);
577
761
  }
762
+ return;
763
+ }
764
+ if (isMac()) {
765
+ if (fs.existsSync(launchdFile())) {
766
+ run('launchctl', ['unload', '-w', launchdFile()]);
767
+ fs.unlinkSync(launchdFile());
768
+ console.log('✔ launchd agent removed');
769
+ }
770
+ else
771
+ console.log('Agent not installed.');
772
+ return;
773
+ }
774
+ if (isWindows()) {
578
775
  try {
579
- execFileSync('nginx', ['-t']);
580
- execFileSync('systemctl', ['reload', 'nginx']);
776
+ execFileSync('schtasks', ['/delete', '/tn', SCHTASK_NAME, '/f'], { stdio: 'inherit' });
777
+ console.log(' scheduled task removed');
778
+ }
779
+ catch {
780
+ console.log('Task not installed.');
581
781
  }
582
- catch { /* nginx may be absent */ }
782
+ return;
583
783
  }
784
+ console.log(`Nothing to uninstall on ${platform()}.`);
584
785
  }
585
786
  // ---------------------------------------------------------------------------
586
787
  // status
587
788
  // ---------------------------------------------------------------------------
588
789
  function cmdStatus() {
589
- const installed = isSystemdInstalled();
590
- console.log(`service installed: ${installed ? 'yes' : 'no'}`);
591
- if (installed) {
592
- console.log(`service status: ${run('systemctl', ['is-active', SERVICE_NAME]).trim() || 'unknown'}`);
593
- }
594
790
  const cfg = loadConfig();
595
- console.log(`listen: ${cfg.host}:${cfg.port}`);
596
- console.log(`tokens: ${cfg.tokens.length}`);
597
- console.log(`audit: ${cfg.audit.enabled ? 'on' : 'off'}`);
598
- console.log(`read_only: ${cfg.read_only ? 'on' : 'off'}`);
791
+ console.log(`platform: ${platformLabel()}`);
792
+ console.log(`config: ${configPath()}`);
793
+ console.log(`${serviceLabel()}:${' '.repeat(Math.max(1, 19 - serviceLabel().length - 1))}${serviceIsInstalled() ? (serviceIsActive() ? 'installed, active' : 'installed, inactive') : 'not installed'}`);
794
+ console.log(`listen: ${cfg.host}:${cfg.port}${cfg.mcp_path}`);
795
+ console.log(`public_host: ${cfg.public_host || '(none — use `ramcp tunnel`)'}`);
796
+ console.log(`tokens: ${cfg.tokens.length}`);
797
+ console.log(`allowed paths: ${cfg.tokens.reduce((n, t) => n + t.allowed_paths.length, 0)}`);
798
+ console.log(`audit: ${cfg.audit.enabled ? 'on' : 'off'}`);
799
+ console.log(`read_only: ${cfg.read_only ? 'on' : 'off'}`);
800
+ }
801
+ // ---------------------------------------------------------------------------
802
+ // schedule / upgrade
803
+ // ---------------------------------------------------------------------------
804
+ function cmdSchedule(args) {
805
+ const p = path.join(configDir(), 'schedule.json');
806
+ if (jsonOut(args)) {
807
+ console.log(fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '[]');
808
+ return;
809
+ }
810
+ try {
811
+ const tasks = JSON.parse(fs.readFileSync(p, 'utf8'));
812
+ if (!tasks.length) {
813
+ console.log('(no scheduled tasks)');
814
+ return;
815
+ }
816
+ for (const t of tasks)
817
+ console.log(`${t.id} | ${t.enabled ? 'on' : 'off'} | ${t.command} | next: ${t.next_run || t.run_at || '-'}`);
818
+ }
819
+ catch {
820
+ console.log('(no scheduled tasks)');
821
+ }
822
+ }
823
+ async function cmdUpgrade(args) {
824
+ console.log(`current: v${PKG.version}`);
825
+ const latest = await fetch('https://registry.npmjs.org/remote-access-mcp/latest')
826
+ .then(r => r.json()).then((j) => j.version)
827
+ .catch(() => null);
828
+ if (!latest) {
829
+ console.error('could not reach npm registry');
830
+ process.exit(1);
831
+ }
832
+ if (latest === PKG.version) {
833
+ console.log('already up to date');
834
+ return;
835
+ }
836
+ console.log(`latest: v${latest}`);
837
+ if (args.flags.has('dry-run')) {
838
+ console.log('(dry-run — no changes)');
839
+ return;
840
+ }
841
+ if (isLinux() && process.getuid?.() !== 0) {
842
+ console.error('upgrade requires root (sudo) for a global npm install');
843
+ process.exit(1);
844
+ }
845
+ execFileSync('npm', ['install', '-g', `remote-access-mcp@${latest}`], { stdio: 'inherit' });
846
+ if (serviceIsInstalled()) {
847
+ if (hasSystemd())
848
+ run('systemctl', ['restart', SERVICE_NAME]);
849
+ else if (isMac()) {
850
+ run('launchctl', ['unload', launchdFile()]);
851
+ run('launchctl', ['load', '-w', launchdFile()]);
852
+ }
853
+ else if (isWindows()) {
854
+ run('schtasks', ['/end', '/tn', SCHTASK_NAME]);
855
+ run('schtasks', ['/run', '/tn', SCHTASK_NAME]);
856
+ }
857
+ console.log(`service restarted on v${latest}`);
858
+ }
599
859
  }
600
860
  // ---------------------------------------------------------------------------
601
861
  // main
@@ -613,6 +873,9 @@ export async function main(argv) {
613
873
  case 'start':
614
874
  await cmdStart(args);
615
875
  break;
876
+ case 'tunnel':
877
+ await cmdTunnel(args);
878
+ break;
616
879
  case 'url':
617
880
  cmdUrl(args);
618
881
  break;
@@ -622,6 +885,9 @@ export async function main(argv) {
622
885
  case 'policy':
623
886
  cmdPolicy(args);
624
887
  break;
888
+ case 'scopes':
889
+ cmdScopes();
890
+ break;
625
891
  case 'audit':
626
892
  cmdAudit(args);
627
893
  break;
@@ -653,63 +919,4 @@ export async function main(argv) {
653
919
  process.exit(1);
654
920
  }
655
921
  }
656
- // ---------------------------------------------------------------------------
657
- // schedule
658
- // ---------------------------------------------------------------------------
659
- function cmdSchedule(args) {
660
- const p = path.join(os.homedir(), '.config', 'remote-access-mcp', 'schedule.json');
661
- if (args.flags.has('json')) {
662
- console.log(fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '[]');
663
- return;
664
- }
665
- try {
666
- const tasks = JSON.parse(fs.readFileSync(p, 'utf8'));
667
- if (!tasks.length) {
668
- console.log('(no scheduled tasks)');
669
- return;
670
- }
671
- for (const t of tasks) {
672
- console.log(`${t.id} | ${t.enabled ? 'on' : 'off'} | ${t.command} | next: ${t.next_run || t.run_at || '-'}`);
673
- }
674
- }
675
- catch {
676
- console.log('(no scheduled tasks)');
677
- }
678
- }
679
- // ---------------------------------------------------------------------------
680
- // upgrade
681
- // ---------------------------------------------------------------------------
682
- async function cmdUpgrade(args) {
683
- const dryRun = args.flags.has('dry-run');
684
- console.log(`current: v${PKG.version}`);
685
- // Check latest from npm registry
686
- const latest = await fetch('https://registry.npmjs.org/remote-access-mcp/latest')
687
- .then(r => r.json())
688
- .then((j) => j.version)
689
- .catch(() => null);
690
- if (!latest) {
691
- console.error('could not reach npm registry');
692
- process.exit(1);
693
- }
694
- if (latest === PKG.version) {
695
- console.log('already up to date');
696
- return;
697
- }
698
- console.log(`latest: v${latest}`);
699
- if (dryRun) {
700
- console.log('(dry-run — no changes made)');
701
- return;
702
- }
703
- if (process.getuid?.() !== 0) {
704
- console.error('upgrade requires root (sudo)');
705
- process.exit(1);
706
- }
707
- console.log('upgrading…');
708
- const { execFileSync } = await import('node:child_process');
709
- execFileSync('npm', ['install', '-g', `remote-access-mcp@${latest}`], { stdio: 'inherit' });
710
- if (isSystemdInstalled()) {
711
- execFileSync('systemctl', ['restart', SERVICE_NAME]);
712
- console.log(`service restarted on v${latest}`);
713
- }
714
- }
715
922
  //# sourceMappingURL=main.js.map