@eightstate/escli 0.8.0 → 0.9.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.
@@ -4,8 +4,7 @@ import { ErrorCode } from '@eightstate/contracts/errors';
4
4
  import { BaseCommand } from '../../base-command.js';
5
5
  import { writeStderr } from '../../io/io.js';
6
6
  import { resolveCliToken } from '../../services/credentials.js';
7
- import { authRequired, DEFAULT_MCP_DOMAIN, maskSecret, usageError } from '../../services/mcp/common.js';
8
- import { CloudflareClient } from '../../services/mcp/cloudflare.js';
7
+ import { authRequired, maskSecret, usageError } from '../../services/mcp/common.js';
9
8
  import { validateRootDirectory, writeMachineConfig } from '../../services/mcp/config.js';
10
9
  import { McpGateClient } from '../../services/mcp/gate-client.js';
11
10
  export const McpRegisterDataSchema = z.object({
@@ -24,7 +23,7 @@ export const McpRegisterDataSchema = z.object({
24
23
  export default class McpRegister extends BaseCommand {
25
24
  static errors = [ErrorCode.AuthRequired, ErrorCode.UsageInvalid, ErrorCode.ConfigInvalid, ErrorCode.ApiError, ErrorCode.GateUnavailable, ErrorCode.GateInvalidResponse, ErrorCode.NetworkError];
26
25
  static summary = 'Register a local directory as an MCP machine';
27
- static examples = ['<%= config.bin %> mcp register --root . --label "repo"', '<%= config.bin %> mcp register --root . --slug my-repo'];
26
+ static examples = ['<%= config.bin %> mcp register --root .', '<%= config.bin %> mcp register --root . --slug my-repo --label "My Repo"'];
28
27
  static enableJsonFlag = true;
29
28
  static strict = true;
30
29
  static flags = {
@@ -32,9 +31,6 @@ export default class McpRegister extends BaseCommand {
32
31
  slug: Flags.string({ description: 'Machine slug. Defaults from --label, root basename, or hostname.' }),
33
32
  port: Flags.integer({ description: 'Local MCP origin port.', default: 9315 }),
34
33
  label: Flags.string({ description: 'Human label for this machine.' }),
35
- 'cf-account-id': Flags.string({ description: 'Cloudflare account ID. Defaults to CLOUDFLARE_ACCOUNT_ID.' }),
36
- 'cf-zone-id': Flags.string({ description: 'Cloudflare zone ID. Defaults to CLOUDFLARE_ZONE_ID.' }),
37
- 'cf-api-token': Flags.string({ description: 'Cloudflare API token. Defaults to CLOUDFLARE_API_TOKEN; never persisted.' }),
38
34
  };
39
35
  async execute() {
40
36
  const flags = await this.parseFlags(McpRegister);
@@ -45,41 +41,25 @@ export default class McpRegister extends BaseCommand {
45
41
  if (port < 1 || port > 65535)
46
42
  throw usageError('--port must be between 1 and 65535');
47
43
  const slug = normalizeSlug(flags.slug ?? flags.label ?? root.split('/').filter(Boolean).pop() ?? 'machine');
48
- const hostname = `mcp-${slug}.${DEFAULT_MCP_DOMAIN}`;
49
- const cf = new CloudflareClient({ accountId: flags['cf-account-id'], zoneId: flags['cf-zone-id'], apiToken: flags['cf-api-token'] });
50
- let tunnelId;
51
- let dnsRecordId;
52
- try {
53
- const tunnel = await cf.createTunnel(slug);
54
- tunnelId = tunnel.id;
55
- await cf.configureIngress(tunnel.id, hostname, port);
56
- const dns = await cf.createDnsRecord(hostname, tunnel.id);
57
- dnsRecordId = dns.id;
58
- const gate = await new McpGateClient().createMachine({ slug, hostname, tunnel_id: tunnel.id, port, label: flags.label });
59
- const config = {
60
- slug,
61
- label: flags.label,
62
- root_default: root,
63
- hostname: gate.hostname,
64
- resource: gate.resource,
65
- tunnel_id: tunnel.id,
66
- tunnel_token: tunnel.token,
67
- dns_record_id: dns.id,
68
- port,
69
- machine_id: gate.machine_id,
70
- machine_secret: gate.machine_secret,
71
- created_at: new Date().toISOString(),
72
- };
73
- const configPath = await writeMachineConfig(config);
74
- if (!flags.quiet && !flags.json)
75
- await writeStderr(`connector URL: ${gate.resource}/mcp\nconfig: ${configPath}\ntunnel: ${maskSecret(tunnel.id)}\n`);
76
- return { slug, label: flags.label, root_default: root, hostname: gate.hostname, resource: gate.resource, port, machine_id: gate.machine_id, tunnel_id: tunnel.id, dns_record_id: dns.id, config_path: configPath, connector_url: `${gate.resource}/mcp` };
77
- }
78
- catch (error) {
79
- const cleanup = [`Cloudflare tunnel id: ${tunnelId ?? '(not created)'}`, `Cloudflare DNS record id: ${dnsRecordId ?? '(not created)'}`].join('\n');
80
- await writeStderr(`MCP register failed. Cleanup references:\n${cleanup}\n`);
81
- throw error;
82
- }
44
+ const gate = await new McpGateClient().createMachine({ slug, port, label: flags.label });
45
+ const config = {
46
+ slug,
47
+ label: flags.label,
48
+ root_default: root,
49
+ hostname: gate.hostname,
50
+ resource: gate.resource,
51
+ tunnel_id: gate.tunnel_id,
52
+ tunnel_token: gate.tunnel_token,
53
+ dns_record_id: gate.dns_record_id,
54
+ port,
55
+ machine_id: gate.machine_id,
56
+ machine_secret: gate.machine_secret,
57
+ created_at: new Date().toISOString(),
58
+ };
59
+ const configPath = await writeMachineConfig(config);
60
+ if (!flags.quiet && !flags.json)
61
+ await writeStderr(`registered: ${gate.hostname}\nconnector URL: ${gate.resource}/mcp\nconfig: ${configPath}\ntunnel: ${maskSecret(gate.tunnel_id)}\n`);
62
+ return { slug, label: flags.label, root_default: root, hostname: gate.hostname, resource: gate.resource, port, machine_id: gate.machine_id, tunnel_id: gate.tunnel_id, dns_record_id: gate.dns_record_id, config_path: configPath, connector_url: `${gate.resource}/mcp` };
83
63
  }
84
64
  }
85
65
  function normalizeSlug(value) {
@@ -2,35 +2,30 @@ import { Flags } from '@oclif/core';
2
2
  import { z } from 'zod';
3
3
  import { ErrorCode } from '@eightstate/contracts/errors';
4
4
  import { BaseCommand } from '../../base-command.js';
5
- import { CloudflareClient } from '../../services/mcp/cloudflare.js';
6
5
  import { deleteMachineConfig, resolveMachineConfig } from '../../services/mcp/config.js';
7
6
  import { McpGateClient } from '../../services/mcp/gate-client.js';
8
7
  export const McpRevokeDataSchema = z.object({ slug: z.string(), gate_revoked: z.boolean(), cloudflare_deleted: z.boolean(), local_config_deleted: z.boolean() });
9
8
  export default class McpRevoke extends BaseCommand {
10
9
  static errors = [ErrorCode.ConfigInvalid, ErrorCode.AuthRequired, ErrorCode.GateUnavailable, ErrorCode.ApiError];
11
- static summary = 'Revoke an MCP machine and optionally delete Cloudflare resources';
12
- static examples = ['<%= config.bin %> mcp revoke --slug my-repo', '<%= config.bin %> mcp revoke --slug my-repo --cloudflare --yes'];
10
+ static summary = 'Revoke an MCP machine and delete its Cloudflare tunnel';
11
+ static examples = ['<%= config.bin %> mcp revoke --slug my-repo', '<%= config.bin %> mcp revoke --slug my-repo --yes'];
13
12
  static enableJsonFlag = true;
14
13
  static strict = true;
15
14
  static flags = {
16
15
  slug: Flags.string({ description: 'Machine slug. Optional only when one config exists.' }),
17
16
  cloudflare: Flags.boolean({ description: 'Also delete Cloudflare DNS record and tunnel.', default: false }),
18
17
  yes: Flags.boolean({ char: 'y', description: 'Skip confirmation for --cloudflare.', default: false }),
19
- 'cf-account-id': Flags.string({ description: 'Cloudflare account ID. Defaults to CLOUDFLARE_ACCOUNT_ID.' }),
20
- 'cf-zone-id': Flags.string({ description: 'Cloudflare zone ID. Defaults to CLOUDFLARE_ZONE_ID.' }),
21
- 'cf-api-token': Flags.string({ description: 'Cloudflare API token. Defaults to CLOUDFLARE_API_TOKEN; never persisted.' }),
22
18
  };
23
19
  async execute() {
24
20
  const flags = await this.parseFlags(McpRevoke);
25
21
  const config = await resolveMachineConfig(flags.slug);
26
- await new McpGateClient().revokeMachine(config.slug);
22
+ const gate = new McpGateClient();
23
+ await gate.revokeMachine(config.slug);
27
24
  let cloudflareDeleted = false;
28
25
  if (flags.cloudflare) {
29
26
  if (!flags.yes)
30
- throw new Error('refusing to delete Cloudflare resources without --yes');
31
- const cf = new CloudflareClient({ accountId: flags['cf-account-id'], zoneId: flags['cf-zone-id'], apiToken: flags['cf-api-token'] });
32
- await cf.deleteDnsRecord(config.dns_record_id);
33
- await cf.deleteTunnel(config.tunnel_id);
27
+ throw new Error('refusing to delete Cloudflare resources without --yes; Gate will deprovision the tunnel and DNS');
28
+ await gate.deprovisionMachine(config.slug);
34
29
  cloudflareDeleted = true;
35
30
  }
36
31
  const localConfigDeleted = await deleteMachineConfig(config.slug);
@@ -3,6 +3,8 @@ import { ExitCodes } from '@eightstate/contracts/exit-codes';
3
3
  import { EscliError } from '../../lib/escli-error.js';
4
4
  import { parseJsonResponse, recordValue, stringValue, usageError } from './common.js';
5
5
  const CF_API_BASE = 'https://api.cloudflare.com/client/v4';
6
+ const EIGHTSTATE_ACCOUNT_ID = '3db5cf3eac3990b5604382b809bb46c6';
7
+ const EIGHTSTATE_ZONE_ID = 'db5d43c916a7db789e1692d1ffdcf12a';
6
8
  export class CloudflareClient {
7
9
  credentials;
8
10
  constructor(input) {
@@ -73,16 +75,11 @@ export class CloudflareClient {
73
75
  }
74
76
  }
75
77
  export function resolveCloudflareCredentials(input) {
76
- const accountId = input.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID;
77
- const zoneId = input.zoneId ?? process.env.CLOUDFLARE_ZONE_ID;
78
+ const accountId = input.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? EIGHTSTATE_ACCOUNT_ID;
79
+ const zoneId = input.zoneId ?? process.env.CLOUDFLARE_ZONE_ID ?? EIGHTSTATE_ZONE_ID;
78
80
  const apiToken = input.apiToken ?? process.env.CLOUDFLARE_API_TOKEN;
79
- const missing = [
80
- accountId ? undefined : 'CLOUDFLARE_ACCOUNT_ID',
81
- zoneId ? undefined : 'CLOUDFLARE_ZONE_ID',
82
- apiToken ? undefined : 'CLOUDFLARE_API_TOKEN',
83
- ].filter(Boolean);
84
- if (!accountId || !zoneId || !apiToken)
85
- throw usageError(`missing Cloudflare credentials: ${missing.join(', ')}`);
81
+ if (!apiToken)
82
+ throw usageError('missing CLOUDFLARE_API_TOKEN set it as an env var or pass --cf-api-token');
86
83
  return { accountId, zoneId, apiToken };
87
84
  }
88
85
  function cfInvalid(message, details) {
@@ -9,6 +9,9 @@ const McpMachineCreateResponseSchema = z.object({
9
9
  machine_secret: z.string(),
10
10
  hostname: z.string(),
11
11
  resource: z.string(),
12
+ tunnel_id: z.string(),
13
+ tunnel_token: z.string(),
14
+ dns_record_id: z.string(),
12
15
  });
13
16
  const McpMachineStatusResponseSchema = z.object({
14
17
  machine_id: z.number(),
@@ -42,6 +45,9 @@ export class McpGateClient {
42
45
  const parsed = McpMachineStatusResponseSchema.safeParse(data);
43
46
  return parsed.success ? parsed.data : data;
44
47
  }
48
+ async deprovisionMachine(slug) {
49
+ await this.request(`/api/mcp/machines/${encodeURIComponent(slug)}/deprovision`, { method: 'POST', userAuth: true });
50
+ }
45
51
  async introspectToken(machineSecret, token, resource) {
46
52
  const data = await this.request('/api/mcp/introspect', {
47
53
  method: 'POST',
@@ -3482,8 +3482,8 @@
3482
3482
  "aliases": [],
3483
3483
  "args": {},
3484
3484
  "examples": [
3485
- "<%= config.bin %> mcp register --root . --label \"repo\"",
3486
- "<%= config.bin %> mcp register --root . --slug my-repo"
3485
+ "<%= config.bin %> mcp register --root .",
3486
+ "<%= config.bin %> mcp register --root . --slug my-repo --label \"My Repo\""
3487
3487
  ],
3488
3488
  "flags": {
3489
3489
  "json": {
@@ -3575,27 +3575,6 @@
3575
3575
  "hasDynamicHelp": false,
3576
3576
  "multiple": false,
3577
3577
  "type": "option"
3578
- },
3579
- "cf-account-id": {
3580
- "description": "Cloudflare account ID. Defaults to CLOUDFLARE_ACCOUNT_ID.",
3581
- "name": "cf-account-id",
3582
- "hasDynamicHelp": false,
3583
- "multiple": false,
3584
- "type": "option"
3585
- },
3586
- "cf-zone-id": {
3587
- "description": "Cloudflare zone ID. Defaults to CLOUDFLARE_ZONE_ID.",
3588
- "name": "cf-zone-id",
3589
- "hasDynamicHelp": false,
3590
- "multiple": false,
3591
- "type": "option"
3592
- },
3593
- "cf-api-token": {
3594
- "description": "Cloudflare API token. Defaults to CLOUDFLARE_API_TOKEN; never persisted.",
3595
- "name": "cf-api-token",
3596
- "hasDynamicHelp": false,
3597
- "multiple": false,
3598
- "type": "option"
3599
3578
  }
3600
3579
  },
3601
3580
  "hasDynamicHelp": false,
@@ -3630,7 +3609,7 @@
3630
3609
  "args": {},
3631
3610
  "examples": [
3632
3611
  "<%= config.bin %> mcp revoke --slug my-repo",
3633
- "<%= config.bin %> mcp revoke --slug my-repo --cloudflare --yes"
3612
+ "<%= config.bin %> mcp revoke --slug my-repo --yes"
3634
3613
  ],
3635
3614
  "flags": {
3636
3615
  "json": {
@@ -3712,27 +3691,6 @@
3712
3691
  "name": "yes",
3713
3692
  "allowNo": false,
3714
3693
  "type": "boolean"
3715
- },
3716
- "cf-account-id": {
3717
- "description": "Cloudflare account ID. Defaults to CLOUDFLARE_ACCOUNT_ID.",
3718
- "name": "cf-account-id",
3719
- "hasDynamicHelp": false,
3720
- "multiple": false,
3721
- "type": "option"
3722
- },
3723
- "cf-zone-id": {
3724
- "description": "Cloudflare zone ID. Defaults to CLOUDFLARE_ZONE_ID.",
3725
- "name": "cf-zone-id",
3726
- "hasDynamicHelp": false,
3727
- "multiple": false,
3728
- "type": "option"
3729
- },
3730
- "cf-api-token": {
3731
- "description": "Cloudflare API token. Defaults to CLOUDFLARE_API_TOKEN; never persisted.",
3732
- "name": "cf-api-token",
3733
- "hasDynamicHelp": false,
3734
- "multiple": false,
3735
- "type": "option"
3736
3694
  }
3737
3695
  },
3738
3696
  "hasDynamicHelp": false,
@@ -3742,7 +3700,7 @@
3742
3700
  "pluginName": "@eightstate/escli",
3743
3701
  "pluginType": "core",
3744
3702
  "strict": true,
3745
- "summary": "Revoke an MCP machine and optionally delete Cloudflare resources",
3703
+ "summary": "Revoke an MCP machine and delete its Cloudflare tunnel",
3746
3704
  "enableJsonFlag": true,
3747
3705
  "emitsJsonEnvelope": false,
3748
3706
  "errors": [
@@ -8062,5 +8020,5 @@
8062
8020
  ]
8063
8021
  }
8064
8022
  },
8065
- "version": "0.8.0"
8023
+ "version": "0.9.0"
8066
8024
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eightstate/escli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "repository": {
@@ -24,7 +24,7 @@
24
24
  "@oclif/core": "4.11.3",
25
25
  "chalk": "5.6.2",
26
26
  "zod": "4.4.3",
27
- "@eightstate/contracts": "0.8.0"
27
+ "@eightstate/contracts": "0.9.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@eslint/js": "9.39.1",