@arbiterai/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/index.js +660 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jaiden Sy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @arbiterai/cli
|
|
2
|
+
|
|
3
|
+
Official CLI for [Arbiter](https://arbiterai.dev) — the developer-first MCP gateway with agent identity, tool-level access control, secrets vault, and full observability.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @arbiterai/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 18+.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Authenticate via browser (device flow)
|
|
17
|
+
arbiter login
|
|
18
|
+
|
|
19
|
+
# Create an agent and save the API key
|
|
20
|
+
arbiter agent create --name my-agent
|
|
21
|
+
|
|
22
|
+
# Grant the agent access to a tool on an MCP server
|
|
23
|
+
arbiter permissions grant --agent <agent-id> --server <server-name> --tool <tool-name>
|
|
24
|
+
|
|
25
|
+
# Store a secret scoped to the agent
|
|
26
|
+
arbiter vault set --agent <agent-id> --key OPENAI_API_KEY --value sk-...
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Command Reference
|
|
30
|
+
|
|
31
|
+
### `arbiter login`
|
|
32
|
+
|
|
33
|
+
Authenticate with Arbiter using the browser-based device flow. Opens a browser tab where you approve access, then stores the token locally at `~/.config/arbiter/config.json`.
|
|
34
|
+
|
|
35
|
+
### `arbiter logout`
|
|
36
|
+
|
|
37
|
+
Clear your local session. Does not revoke the token server-side.
|
|
38
|
+
|
|
39
|
+
### `arbiter status [--json]`
|
|
40
|
+
|
|
41
|
+
Show current auth state and gateway health.
|
|
42
|
+
|
|
43
|
+
| Flag | Description |
|
|
44
|
+
|------|-------------|
|
|
45
|
+
| `--json` | Output raw JSON |
|
|
46
|
+
|
|
47
|
+
### `arbiter agent create --name <name> [--json]`
|
|
48
|
+
|
|
49
|
+
Create a new agent. Prints the agent ID and a one-time API key — save it immediately.
|
|
50
|
+
|
|
51
|
+
| Flag | Description |
|
|
52
|
+
|------|-------------|
|
|
53
|
+
| `--name <name>` | Display name for the agent (required) |
|
|
54
|
+
| `--json` | Output raw JSON |
|
|
55
|
+
|
|
56
|
+
### `arbiter agent list [--json]`
|
|
57
|
+
|
|
58
|
+
List all agents in your org.
|
|
59
|
+
|
|
60
|
+
| Flag | Description |
|
|
61
|
+
|------|-------------|
|
|
62
|
+
| `--json` | Output raw JSON |
|
|
63
|
+
|
|
64
|
+
### `arbiter agent delete <id>`
|
|
65
|
+
|
|
66
|
+
Delete an agent by ID. Prompts for confirmation before proceeding.
|
|
67
|
+
|
|
68
|
+
### `arbiter permissions grant --agent <id> --server <name> --tool <tool> [--json]`
|
|
69
|
+
|
|
70
|
+
Grant a tool permission to an agent on a named MCP server. Use `*` as the tool name to allow all tools on that server.
|
|
71
|
+
|
|
72
|
+
| Flag | Description |
|
|
73
|
+
|------|-------------|
|
|
74
|
+
| `--agent <id>` | Agent ID (required) |
|
|
75
|
+
| `--server <name>` | MCP server name (required) |
|
|
76
|
+
| `--tool <tool>` | Tool name, or `*` for all tools (required) |
|
|
77
|
+
| `--json` | Output raw JSON |
|
|
78
|
+
|
|
79
|
+
### `arbiter permissions list --agent <id> [--json]`
|
|
80
|
+
|
|
81
|
+
List all permissions granted to an agent.
|
|
82
|
+
|
|
83
|
+
| Flag | Description |
|
|
84
|
+
|------|-------------|
|
|
85
|
+
| `--agent <id>` | Agent ID (required) |
|
|
86
|
+
| `--json` | Output raw JSON |
|
|
87
|
+
|
|
88
|
+
### `arbiter vault set --agent <id> --key <key> --value <value> [--json]`
|
|
89
|
+
|
|
90
|
+
Store a secret in the vault scoped to an agent. Key must match `[A-Za-z0-9_]+`.
|
|
91
|
+
|
|
92
|
+
To avoid the secret appearing in shell history, use:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
read -s VAL && arbiter vault set --agent <id> --key MY_KEY --value "$VAL"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Flag | Description |
|
|
99
|
+
|------|-------------|
|
|
100
|
+
| `--agent <id>` | Agent ID to scope the secret to (required) |
|
|
101
|
+
| `--key <key>` | Secret key name, e.g. `OPENAI_API_KEY` (required) |
|
|
102
|
+
| `--value <value>` | Secret value (required) |
|
|
103
|
+
| `--json` | Output raw JSON |
|
|
104
|
+
|
|
105
|
+
## Auth
|
|
106
|
+
|
|
107
|
+
Arbiter CLI uses the [OAuth 2.0 Device Authorization Grant](https://datatracker.ietf.org/doc/html/rfc8628) flow:
|
|
108
|
+
|
|
109
|
+
1. `arbiter login` calls the Arbiter API to initiate a device flow and prints a short code.
|
|
110
|
+
2. Your browser opens to `https://arbiterai.dev/cli-auth?code=<code>` where you approve access.
|
|
111
|
+
3. The CLI polls for the token (every 3 seconds, up to 15 minutes).
|
|
112
|
+
4. On success, the token and org ID are written to `~/.config/arbiter/config.json` (mode `0600`).
|
|
113
|
+
|
|
114
|
+
To target a self-hosted or staging instance:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
arbiter --api-url https://your-instance.example.com login
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Or set the environment variable:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
export ARBITER_API_URL=https://your-instance.example.com
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
AGPL-3.0-or-later
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
|
|
29
|
+
// package.json
|
|
30
|
+
var require_package = __commonJS({
|
|
31
|
+
"package.json"(exports2, module2) {
|
|
32
|
+
module2.exports = {
|
|
33
|
+
name: "@arbiterai/cli",
|
|
34
|
+
version: "0.1.0",
|
|
35
|
+
description: "Official CLI for Arbiter \u2014 the developer-first MCP gateway",
|
|
36
|
+
license: "MIT",
|
|
37
|
+
keywords: [
|
|
38
|
+
"mcp",
|
|
39
|
+
"ai",
|
|
40
|
+
"gateway",
|
|
41
|
+
"arbiter",
|
|
42
|
+
"cli"
|
|
43
|
+
],
|
|
44
|
+
repository: {
|
|
45
|
+
type: "git",
|
|
46
|
+
url: "git+https://github.com/JaidenSy/arbiter-cli.git"
|
|
47
|
+
},
|
|
48
|
+
type: "commonjs",
|
|
49
|
+
bin: {
|
|
50
|
+
arbiter: "dist/index.js"
|
|
51
|
+
},
|
|
52
|
+
main: "./dist/index.js",
|
|
53
|
+
files: [
|
|
54
|
+
"dist"
|
|
55
|
+
],
|
|
56
|
+
engines: {
|
|
57
|
+
node: ">=18.0.0"
|
|
58
|
+
},
|
|
59
|
+
scripts: {
|
|
60
|
+
build: "tsup",
|
|
61
|
+
dev: "tsup --watch",
|
|
62
|
+
lint: "eslint src --max-warnings 0",
|
|
63
|
+
"type-check": "tsc --noEmit",
|
|
64
|
+
prepublishOnly: "npm run build"
|
|
65
|
+
},
|
|
66
|
+
dependencies: {
|
|
67
|
+
axios: "^1.7.7",
|
|
68
|
+
chalk: "^5.3.0",
|
|
69
|
+
"cli-table3": "^0.6.5",
|
|
70
|
+
commander: "^12.1.0",
|
|
71
|
+
open: "^10.1.0",
|
|
72
|
+
ora: "^8.1.0"
|
|
73
|
+
},
|
|
74
|
+
devDependencies: {
|
|
75
|
+
"@eslint/js": "^9.13.0",
|
|
76
|
+
"@types/node": "^22.0.0",
|
|
77
|
+
eslint: "^9.13.0",
|
|
78
|
+
tsup: "^8.3.0",
|
|
79
|
+
typescript: "^5.6.3",
|
|
80
|
+
"typescript-eslint": "^8.57.2"
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// src/index.ts
|
|
87
|
+
var import_commander = require("commander");
|
|
88
|
+
|
|
89
|
+
// src/lib/api.ts
|
|
90
|
+
var import_axios = __toESM(require("axios"));
|
|
91
|
+
|
|
92
|
+
// src/lib/config.ts
|
|
93
|
+
var import_fs = __toESM(require("fs"));
|
|
94
|
+
var import_os = __toESM(require("os"));
|
|
95
|
+
var import_path = __toESM(require("path"));
|
|
96
|
+
var DEFAULT_API_URL = "https://nexusai-api-production.up.railway.app";
|
|
97
|
+
function getConfigDir() {
|
|
98
|
+
return import_path.default.join(import_os.default.homedir(), ".config", "arbiter");
|
|
99
|
+
}
|
|
100
|
+
function getConfigPath() {
|
|
101
|
+
return import_path.default.join(getConfigDir(), "config.json");
|
|
102
|
+
}
|
|
103
|
+
function getConfig() {
|
|
104
|
+
try {
|
|
105
|
+
const raw = import_fs.default.readFileSync(getConfigPath(), "utf-8");
|
|
106
|
+
return JSON.parse(raw);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function setConfig(data) {
|
|
112
|
+
const dir = getConfigDir();
|
|
113
|
+
if (!import_fs.default.existsSync(dir)) {
|
|
114
|
+
import_fs.default.mkdirSync(dir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
const existing = getConfig() ?? { api_url: resolveApiUrl(), access_token: "", org_id: "" };
|
|
117
|
+
const updated = { ...existing, ...data };
|
|
118
|
+
const filePath = getConfigPath();
|
|
119
|
+
import_fs.default.writeFileSync(filePath, JSON.stringify(updated, null, 2), "utf-8");
|
|
120
|
+
import_fs.default.chmodSync(filePath, 384);
|
|
121
|
+
}
|
|
122
|
+
function clearConfig() {
|
|
123
|
+
const filePath = getConfigPath();
|
|
124
|
+
if (import_fs.default.existsSync(filePath)) {
|
|
125
|
+
import_fs.default.unlinkSync(filePath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function isLoggedIn() {
|
|
129
|
+
const cfg = getConfig();
|
|
130
|
+
return !!cfg?.access_token;
|
|
131
|
+
}
|
|
132
|
+
function requireAuth() {
|
|
133
|
+
const cfg = getConfig();
|
|
134
|
+
if (!cfg?.access_token) {
|
|
135
|
+
console.error("Not logged in. Run `arbiter login`.");
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
return cfg;
|
|
139
|
+
}
|
|
140
|
+
function resolveApiUrl() {
|
|
141
|
+
const envUrl = process.env["ARBITER_API_URL"];
|
|
142
|
+
if (envUrl) return envUrl;
|
|
143
|
+
const cfg = getConfig();
|
|
144
|
+
if (cfg?.api_url) return cfg.api_url;
|
|
145
|
+
return DEFAULT_API_URL;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/lib/api.ts
|
|
149
|
+
var ApiError = class extends Error {
|
|
150
|
+
constructor(status, detail) {
|
|
151
|
+
super(detail);
|
|
152
|
+
this.status = status;
|
|
153
|
+
this.detail = detail;
|
|
154
|
+
this.name = "ApiError";
|
|
155
|
+
}
|
|
156
|
+
status;
|
|
157
|
+
detail;
|
|
158
|
+
};
|
|
159
|
+
function createClient() {
|
|
160
|
+
const client2 = import_axios.default.create({
|
|
161
|
+
baseURL: resolveApiUrl(),
|
|
162
|
+
headers: { "Content-Type": "application/json" }
|
|
163
|
+
});
|
|
164
|
+
client2.interceptors.request.use((config) => {
|
|
165
|
+
const cfg = getConfig();
|
|
166
|
+
if (cfg?.access_token) {
|
|
167
|
+
config.headers["Authorization"] = `Bearer ${cfg.access_token}`;
|
|
168
|
+
}
|
|
169
|
+
return config;
|
|
170
|
+
});
|
|
171
|
+
client2.interceptors.response.use(
|
|
172
|
+
(res) => res,
|
|
173
|
+
(err) => {
|
|
174
|
+
const status = err.response?.status ?? 0;
|
|
175
|
+
const detail = err.response?.data?.detail || err.message || "Unknown error";
|
|
176
|
+
if (status === 401) {
|
|
177
|
+
console.error("Session expired. Run `arbiter login`.");
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
throw new ApiError(status, detail);
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
return client2;
|
|
184
|
+
}
|
|
185
|
+
var _client = null;
|
|
186
|
+
function client() {
|
|
187
|
+
if (!_client) {
|
|
188
|
+
_client = createClient();
|
|
189
|
+
}
|
|
190
|
+
return _client;
|
|
191
|
+
}
|
|
192
|
+
function resetClient() {
|
|
193
|
+
_client = null;
|
|
194
|
+
}
|
|
195
|
+
async function get(path2, params) {
|
|
196
|
+
const res = await client().get(path2, { params });
|
|
197
|
+
return res.data;
|
|
198
|
+
}
|
|
199
|
+
async function post(path2, body) {
|
|
200
|
+
const res = await client().post(path2, body);
|
|
201
|
+
return res.data;
|
|
202
|
+
}
|
|
203
|
+
async function del(path2) {
|
|
204
|
+
await client().delete(path2);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/lib/auth.ts
|
|
208
|
+
var import_axios2 = __toESM(require("axios"));
|
|
209
|
+
var import_ora = __toESM(require("ora"));
|
|
210
|
+
var import_open = __toESM(require("open"));
|
|
211
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
212
|
+
var MAX_POLL_MS = 15 * 60 * 1e3;
|
|
213
|
+
async function deviceFlow() {
|
|
214
|
+
const baseUrl = resolveApiUrl();
|
|
215
|
+
const initRes = await import_axios2.default.post(
|
|
216
|
+
`${baseUrl}/api/v1/auth/cli/device`,
|
|
217
|
+
{},
|
|
218
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
219
|
+
);
|
|
220
|
+
const { device_code, user_code, verification_uri } = initRes.data;
|
|
221
|
+
const verificationUri = `${verification_uri}?code=${user_code}`;
|
|
222
|
+
console.log("");
|
|
223
|
+
console.log(" Open your browser to authorize:");
|
|
224
|
+
console.log("");
|
|
225
|
+
console.log(` ${verificationUri}`);
|
|
226
|
+
console.log("");
|
|
227
|
+
console.log(` Or visit the URL manually and enter code: ${user_code}`);
|
|
228
|
+
console.log("");
|
|
229
|
+
try {
|
|
230
|
+
await (0, import_open.default)(verificationUri);
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
const spinner = (0, import_ora.default)("Waiting for authorization... (press Ctrl+C to cancel)").start();
|
|
234
|
+
const deadline = Date.now() + MAX_POLL_MS;
|
|
235
|
+
let networkErrorCount = 0;
|
|
236
|
+
while (Date.now() < deadline) {
|
|
237
|
+
await sleep(POLL_INTERVAL_MS);
|
|
238
|
+
try {
|
|
239
|
+
const pollRes = await import_axios2.default.post(
|
|
240
|
+
`${baseUrl}/api/v1/auth/cli/token`,
|
|
241
|
+
{ device_code },
|
|
242
|
+
{
|
|
243
|
+
headers: { "Content-Type": "application/json" },
|
|
244
|
+
validateStatus: (s) => [200, 428, 410, 403].includes(s)
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
networkErrorCount = 0;
|
|
248
|
+
if (pollRes.status === 200) {
|
|
249
|
+
spinner.succeed("Authorization granted.");
|
|
250
|
+
const token = pollRes.data;
|
|
251
|
+
setConfig({
|
|
252
|
+
access_token: token.access_token,
|
|
253
|
+
org_id: token.org_id,
|
|
254
|
+
logged_in_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
255
|
+
});
|
|
256
|
+
return token;
|
|
257
|
+
}
|
|
258
|
+
if (pollRes.status === 428) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (pollRes.status === 410) {
|
|
262
|
+
spinner.fail("Authorization expired.");
|
|
263
|
+
console.error("Authorization expired. Run `arbiter login` again.");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
if (pollRes.status === 403) {
|
|
267
|
+
spinner.fail("Authorization denied.");
|
|
268
|
+
console.error("Authorization denied.");
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
networkErrorCount++;
|
|
273
|
+
if (networkErrorCount >= 3) {
|
|
274
|
+
spinner.fail("Network error.");
|
|
275
|
+
console.error("Could not reach Arbiter API. Check your connection.");
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
spinner.fail("Authorization timed out.");
|
|
281
|
+
console.error("Authorization timed out. Run `arbiter login` to try again.");
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
function sleep(ms) {
|
|
285
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/lib/output.ts
|
|
289
|
+
var import_chalk = __toESM(require("chalk"));
|
|
290
|
+
var import_cli_table3 = __toESM(require("cli-table3"));
|
|
291
|
+
function printTable(headers, rows) {
|
|
292
|
+
const table = new import_cli_table3.default({
|
|
293
|
+
head: headers.map((h) => import_chalk.default.bold.cyan(h)),
|
|
294
|
+
style: { border: ["grey"], head: [] }
|
|
295
|
+
});
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
table.push(row);
|
|
298
|
+
}
|
|
299
|
+
console.log(table.toString());
|
|
300
|
+
}
|
|
301
|
+
function printSuccess(msg) {
|
|
302
|
+
console.log(import_chalk.default.green("\u2713") + " " + msg);
|
|
303
|
+
}
|
|
304
|
+
function printError(msg) {
|
|
305
|
+
console.error(import_chalk.default.red("\u2717") + " " + msg);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
function printWarning(msg) {
|
|
309
|
+
console.warn(import_chalk.default.yellow("\u26A0") + " " + msg);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/commands/login.ts
|
|
313
|
+
function registerLogin(program2) {
|
|
314
|
+
program2.command("login").description("Authenticate with Arbiter via browser").action(async () => {
|
|
315
|
+
await deviceFlow();
|
|
316
|
+
try {
|
|
317
|
+
const me = await get("/api/v1/auth/me");
|
|
318
|
+
setConfig({ user_email: me.email });
|
|
319
|
+
printSuccess(`Logged in as ${me.email} (org: ${me.org_id})`);
|
|
320
|
+
} catch {
|
|
321
|
+
printSuccess("Logged in successfully.");
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/commands/logout.ts
|
|
327
|
+
function registerLogout(program2) {
|
|
328
|
+
program2.command("logout").description("Clear your local Arbiter session").action(() => {
|
|
329
|
+
if (!isLoggedIn()) {
|
|
330
|
+
console.log("Not logged in.");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
clearConfig();
|
|
334
|
+
printSuccess("Logged out.");
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/commands/status.ts
|
|
339
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
340
|
+
function registerStatus(program2) {
|
|
341
|
+
program2.command("status").description("Show current auth state and gateway health").option("--json", "Output raw JSON").action(async (opts) => {
|
|
342
|
+
const cfg = getConfig();
|
|
343
|
+
const apiUrl = resolveApiUrl();
|
|
344
|
+
if (!cfg?.access_token) {
|
|
345
|
+
if (opts.json) {
|
|
346
|
+
console.log(JSON.stringify({ logged_in: false, api_url: apiUrl }));
|
|
347
|
+
} else {
|
|
348
|
+
console.log("Not logged in. Run `arbiter login`.");
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let gatewayStatus = "unknown";
|
|
353
|
+
try {
|
|
354
|
+
const health = await get("/health");
|
|
355
|
+
gatewayStatus = health.status;
|
|
356
|
+
} catch {
|
|
357
|
+
gatewayStatus = "unreachable";
|
|
358
|
+
}
|
|
359
|
+
let me = null;
|
|
360
|
+
try {
|
|
361
|
+
me = await get("/api/v1/auth/me");
|
|
362
|
+
} catch {
|
|
363
|
+
console.error("Session expired. Run `arbiter login`.");
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
if (opts.json) {
|
|
367
|
+
console.log(
|
|
368
|
+
JSON.stringify({
|
|
369
|
+
logged_in: true,
|
|
370
|
+
email: me?.email ?? null,
|
|
371
|
+
org_id: me?.org_id ?? cfg.org_id,
|
|
372
|
+
api_url: apiUrl,
|
|
373
|
+
gateway_status: gatewayStatus
|
|
374
|
+
})
|
|
375
|
+
);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
console.log(import_chalk2.default.green("Logged in"));
|
|
379
|
+
console.log(` Email: ${me?.email ?? "(unknown)"}`);
|
|
380
|
+
console.log(` Org ID: ${me?.org_id ?? cfg.org_id}`);
|
|
381
|
+
console.log(` Plan: ${me?.org_plan ?? "(unknown)"}`);
|
|
382
|
+
console.log(` API: ${apiUrl}`);
|
|
383
|
+
console.log(` Gateway status: ${gatewayStatus === "ok" ? import_chalk2.default.green(gatewayStatus) : import_chalk2.default.yellow(gatewayStatus)}`);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/commands/agent/create.ts
|
|
388
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
389
|
+
function registerAgentCreate(agentCmd) {
|
|
390
|
+
agentCmd.command("create").description("Create a new agent").requiredOption("--name <name>", "Display name for the agent").option("--json", "Output raw JSON").action(async (opts) => {
|
|
391
|
+
requireAuth();
|
|
392
|
+
let agent;
|
|
393
|
+
try {
|
|
394
|
+
agent = await post("/api/v1/agents", { name: opts.name });
|
|
395
|
+
} catch (err) {
|
|
396
|
+
if (err instanceof ApiError) {
|
|
397
|
+
if (err.status === 402) {
|
|
398
|
+
printError("Plan limit reached. Upgrade at https://arbiterai.dev/pricing");
|
|
399
|
+
}
|
|
400
|
+
if (err.status === 409) {
|
|
401
|
+
printError("An agent with that name already exists.");
|
|
402
|
+
}
|
|
403
|
+
printError(`API error: ${err.detail}`);
|
|
404
|
+
}
|
|
405
|
+
throw err;
|
|
406
|
+
}
|
|
407
|
+
if (opts.json) {
|
|
408
|
+
console.log(JSON.stringify(agent, null, 2));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
printSuccess("Agent created");
|
|
412
|
+
console.log(` ID: ${agent.id}`);
|
|
413
|
+
console.log(` Name: ${agent.name}`);
|
|
414
|
+
console.log("");
|
|
415
|
+
console.log(
|
|
416
|
+
import_chalk3.default.yellow("\u26A0 Save this API key \u2014 it will not be shown again:")
|
|
417
|
+
);
|
|
418
|
+
console.log("");
|
|
419
|
+
console.log(` ${import_chalk3.default.bold(agent.api_key ?? "(key not returned)")}`);
|
|
420
|
+
console.log("");
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/commands/agent/list.ts
|
|
425
|
+
function registerAgentList(agentCmd) {
|
|
426
|
+
agentCmd.command("list").description("List all agents in your org").option("--json", "Output raw JSON").action(async (opts) => {
|
|
427
|
+
requireAuth();
|
|
428
|
+
let page;
|
|
429
|
+
try {
|
|
430
|
+
page = await get("/api/v1/agents", { skip: 0, limit: 200 });
|
|
431
|
+
} catch (err) {
|
|
432
|
+
if (err instanceof ApiError) {
|
|
433
|
+
printError(err.message);
|
|
434
|
+
}
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
if (opts.json) {
|
|
438
|
+
console.log(JSON.stringify(page, null, 2));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (page.items.length === 0) {
|
|
442
|
+
printTable(["ID", "Name", "Scope", "Created"], []);
|
|
443
|
+
console.log("No agents found. Create one with 'arbiter agent create --name <name>'.");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const rows = page.items.map((a) => [
|
|
447
|
+
a.id,
|
|
448
|
+
a.name,
|
|
449
|
+
a.scope,
|
|
450
|
+
formatDate(a.created_at)
|
|
451
|
+
]);
|
|
452
|
+
printTable(["ID", "Name", "Scope", "Created"], rows);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function formatDate(iso) {
|
|
456
|
+
const d = new Date(iso);
|
|
457
|
+
return d.toISOString().replace("T", " ").slice(0, 16);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/commands/agent/delete.ts
|
|
461
|
+
var readline = __toESM(require("readline"));
|
|
462
|
+
function registerAgentDelete(agentCmd) {
|
|
463
|
+
agentCmd.command("delete <id>").description("Delete an agent by ID").action(async (id) => {
|
|
464
|
+
requireAuth();
|
|
465
|
+
const confirmed = await confirm(`Delete agent ${id}? (y/N) `);
|
|
466
|
+
if (!confirmed) {
|
|
467
|
+
console.log("Aborted.");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
await del(`/api/v1/agents/${id}`);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
if (err instanceof ApiError) {
|
|
474
|
+
if (err.status === 404) {
|
|
475
|
+
printError("Agent not found.");
|
|
476
|
+
}
|
|
477
|
+
printError(`API error: ${err.detail}`);
|
|
478
|
+
}
|
|
479
|
+
throw err;
|
|
480
|
+
}
|
|
481
|
+
printSuccess(`Agent ${id} deleted.`);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function confirm(prompt) {
|
|
485
|
+
return new Promise((resolve) => {
|
|
486
|
+
const rl = readline.createInterface({
|
|
487
|
+
input: process.stdin,
|
|
488
|
+
output: process.stdout
|
|
489
|
+
});
|
|
490
|
+
rl.question(prompt, (answer) => {
|
|
491
|
+
rl.close();
|
|
492
|
+
resolve(answer.toLowerCase() === "y");
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/commands/agent/index.ts
|
|
498
|
+
function registerAgentCommands(program2) {
|
|
499
|
+
const agentCmd = program2.command("agent").description("Manage agents in your org");
|
|
500
|
+
registerAgentCreate(agentCmd);
|
|
501
|
+
registerAgentList(agentCmd);
|
|
502
|
+
registerAgentDelete(agentCmd);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/commands/permissions/grant.ts
|
|
506
|
+
function registerPermissionsGrant(permissionsCmd) {
|
|
507
|
+
permissionsCmd.command("grant").description("Grant a tool permission to an agent").requiredOption("--agent <id>", "Agent ID").requiredOption("--tool <tool>", "Tool name (e.g. read_file, or * for all tools)").requiredOption("--server <name>", "MCP server name").option("--json", "Output raw JSON").action(async (opts) => {
|
|
508
|
+
requireAuth();
|
|
509
|
+
const serverPage = await get("/api/v1/mcp-servers", { skip: 0, limit: 200 });
|
|
510
|
+
const server = serverPage.items.find(
|
|
511
|
+
(s) => s.name.toLowerCase() === opts.server.toLowerCase()
|
|
512
|
+
);
|
|
513
|
+
if (!server) {
|
|
514
|
+
printError(`MCP server '${opts.server}' not found. Verify the server name against your Arbiter dashboard.`);
|
|
515
|
+
}
|
|
516
|
+
let permission;
|
|
517
|
+
try {
|
|
518
|
+
permission = await post(`/api/v1/agents/${opts.agent}/permissions`, {
|
|
519
|
+
mcp_server_id: server.id,
|
|
520
|
+
tool_name: opts.tool
|
|
521
|
+
});
|
|
522
|
+
} catch (err) {
|
|
523
|
+
if (err instanceof ApiError) {
|
|
524
|
+
if (err.status === 404) {
|
|
525
|
+
printError("Agent or MCP server not found.");
|
|
526
|
+
}
|
|
527
|
+
if (err.status === 409) {
|
|
528
|
+
console.log("Permission already exists.");
|
|
529
|
+
process.exit(0);
|
|
530
|
+
}
|
|
531
|
+
printError(`API error: ${err.detail}`);
|
|
532
|
+
}
|
|
533
|
+
throw err;
|
|
534
|
+
}
|
|
535
|
+
if (opts.json) {
|
|
536
|
+
console.log(JSON.stringify(permission, null, 2));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
printSuccess(`Permission granted: ${opts.agent} \u2192 ${opts.server}/${opts.tool}`);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/commands/permissions/list.ts
|
|
544
|
+
function registerPermissionsList(permissionsCmd) {
|
|
545
|
+
permissionsCmd.command("list").description("List permissions for an agent").requiredOption("--agent <id>", "Agent ID").option("--json", "Output raw JSON").action(async (opts) => {
|
|
546
|
+
requireAuth();
|
|
547
|
+
let page;
|
|
548
|
+
try {
|
|
549
|
+
page = await get(`/api/v1/agents/${opts.agent}/permissions`);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
if (err instanceof ApiError) {
|
|
552
|
+
if (err.status === 404) {
|
|
553
|
+
printError("Agent not found.");
|
|
554
|
+
}
|
|
555
|
+
printError(`API error: ${err.detail}`);
|
|
556
|
+
}
|
|
557
|
+
throw err;
|
|
558
|
+
}
|
|
559
|
+
if (opts.json) {
|
|
560
|
+
console.log(JSON.stringify(page, null, 2));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (page.items.length === 0) {
|
|
564
|
+
printTable(["Tool", "Server ID", "Granted At"], []);
|
|
565
|
+
console.log("No permissions granted for this agent.");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const rows = page.items.map((p) => [
|
|
569
|
+
p.tool_name,
|
|
570
|
+
p.mcp_server_id,
|
|
571
|
+
formatDate2(p.granted_at)
|
|
572
|
+
]);
|
|
573
|
+
printTable(["Tool", "Server ID", "Granted At"], rows);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function formatDate2(iso) {
|
|
577
|
+
const d = new Date(iso);
|
|
578
|
+
return d.toISOString().replace("T", " ").slice(0, 16);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/commands/permissions/index.ts
|
|
582
|
+
function registerPermissionsCommands(program2) {
|
|
583
|
+
const permissionsCmd = program2.command("permissions").description("Manage agent tool permissions");
|
|
584
|
+
registerPermissionsGrant(permissionsCmd);
|
|
585
|
+
registerPermissionsList(permissionsCmd);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/commands/vault/set.ts
|
|
589
|
+
var SECRET_NAME_RE = /^[A-Za-z0-9_]+$/;
|
|
590
|
+
function registerVaultSet(vaultCmd) {
|
|
591
|
+
vaultCmd.command("set").description("Store a secret in the vault").requiredOption("--agent <id>", "Agent ID to scope the secret to").requiredOption("--key <key>", "Secret key name (e.g. OPENAI_API_KEY)").requiredOption("--value <value>", "Secret value").option("--json", "Output raw JSON").action(async (opts) => {
|
|
592
|
+
requireAuth();
|
|
593
|
+
if (!SECRET_NAME_RE.test(opts.key)) {
|
|
594
|
+
printError(
|
|
595
|
+
"Secret key must contain only letters, numbers, and underscores (A-Za-z0-9_)."
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
if (process.stdout.isTTY) {
|
|
599
|
+
printWarning(
|
|
600
|
+
"Secret value provided via --value flag will appear in shell history."
|
|
601
|
+
);
|
|
602
|
+
console.warn(
|
|
603
|
+
` Consider using: read -s VAL && arbiter vault set --agent ${opts.agent} --key ${opts.key} --value "$VAL"`
|
|
604
|
+
);
|
|
605
|
+
console.warn("");
|
|
606
|
+
}
|
|
607
|
+
let secret;
|
|
608
|
+
try {
|
|
609
|
+
secret = await post("/api/v1/vault/secrets", {
|
|
610
|
+
name: opts.key,
|
|
611
|
+
value: opts.value,
|
|
612
|
+
agent_id: opts.agent
|
|
613
|
+
});
|
|
614
|
+
} catch (err) {
|
|
615
|
+
if (err instanceof ApiError) {
|
|
616
|
+
if (err.status === 404) {
|
|
617
|
+
printError("Agent not found.");
|
|
618
|
+
}
|
|
619
|
+
if (err.status === 402) {
|
|
620
|
+
printError("Plan limit reached. Upgrade at https://arbiterai.dev/pricing");
|
|
621
|
+
}
|
|
622
|
+
printError(`API error: ${err.detail}`);
|
|
623
|
+
}
|
|
624
|
+
throw err;
|
|
625
|
+
}
|
|
626
|
+
if (opts.json) {
|
|
627
|
+
console.log(JSON.stringify(secret, null, 2));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
printSuccess(`Secret '${opts.key}' set for agent ${opts.agent}.`);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/commands/vault/index.ts
|
|
635
|
+
function registerVaultCommands(program2) {
|
|
636
|
+
const vaultCmd = program2.command("vault").description("Manage secrets in the vault");
|
|
637
|
+
registerVaultSet(vaultCmd);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/index.ts
|
|
641
|
+
var pkg = require_package();
|
|
642
|
+
var program = new import_commander.Command();
|
|
643
|
+
program.name("arbiter").description("Arbiter CLI \u2014 manage your MCP gateway from the terminal").version(pkg.version, "-v, --version").option("--api-url <url>", "Override API base URL", (url) => {
|
|
644
|
+
process.env["ARBITER_API_URL"] = url;
|
|
645
|
+
resetClient();
|
|
646
|
+
});
|
|
647
|
+
registerLogin(program);
|
|
648
|
+
registerLogout(program);
|
|
649
|
+
registerStatus(program);
|
|
650
|
+
registerAgentCommands(program);
|
|
651
|
+
registerPermissionsCommands(program);
|
|
652
|
+
registerVaultCommands(program);
|
|
653
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
654
|
+
if (err instanceof Error) {
|
|
655
|
+
console.error(err.message);
|
|
656
|
+
} else {
|
|
657
|
+
console.error("An unexpected error occurred.");
|
|
658
|
+
}
|
|
659
|
+
process.exit(1);
|
|
660
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arbiterai/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official CLI for Arbiter — the developer-first MCP gateway",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"ai",
|
|
9
|
+
"gateway",
|
|
10
|
+
"arbiter",
|
|
11
|
+
"cli"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/JaidenSy/arbiter-cli.git"
|
|
16
|
+
},
|
|
17
|
+
"type": "commonjs",
|
|
18
|
+
"bin": {
|
|
19
|
+
"arbiter": "dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"dev": "tsup --watch",
|
|
31
|
+
"lint": "eslint src --max-warnings 0",
|
|
32
|
+
"type-check": "tsc --noEmit",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"axios": "^1.7.7",
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"cli-table3": "^0.6.5",
|
|
39
|
+
"commander": "^12.1.0",
|
|
40
|
+
"open": "^10.1.0",
|
|
41
|
+
"ora": "^8.1.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^9.13.0",
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"eslint": "^9.13.0",
|
|
47
|
+
"tsup": "^8.3.0",
|
|
48
|
+
"typescript": "^5.6.3",
|
|
49
|
+
"typescript-eslint": "^8.57.2"
|
|
50
|
+
}
|
|
51
|
+
}
|