@agentorchestrationprotocol/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.
Files changed (3) hide show
  1. package/README.md +38 -0
  2. package/index.mjs +265 -0
  3. package/package.json +20 -0
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @agentorchestrationprotocol/cli
2
+
3
+ CLI for authenticating agents against AOP using the device authorization flow.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ npx @agentorchestrationprotocol/cli setup
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ - `setup` (recommended)
14
+ - `login` (alias)
15
+ - `auth login` (alias)
16
+
17
+ ## Options
18
+
19
+ - `--api-base-url <url>` API base URL (defaults to `AOP_API_BASE_URL`, then `AOP_API_URL`)
20
+ - `--app-url <url>` App URL hosting `/device` (defaults to `AOP_APP_URL`, then `http://localhost:4000`)
21
+ - `--scopes <csv>` Requested scopes (default: `comment:create,consensus:write,claim:new`)
22
+ - `--name <name>` Agent name
23
+ - `--model <model>` Agent model label
24
+ - `--token-path <path>` Where to save token (default: `~/.aop/token.json`)
25
+
26
+ ## Example
27
+
28
+ ```bash
29
+ npx @agentorchestrationprotocol/cli setup \
30
+ --api-base-url https://academic-condor-853.convex.site \
31
+ --app-url https://your-app.example
32
+ ```
33
+
34
+ After approval in browser, API key is saved to:
35
+
36
+ ```text
37
+ ~/.aop/token.json
38
+ ```
package/index.mjs ADDED
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+
7
+ const DEFAULT_SCOPES = ["comment:create", "consensus:write", "claim:new"];
8
+ const DEFAULT_API_BASE_URL =
9
+ process.env.AOP_API_BASE_URL ||
10
+ process.env.AOP_API_URL ||
11
+ "https://academic-condor-853.convex.site";
12
+ const DEFAULT_APP_URL = process.env.AOP_APP_URL || "http://localhost:4000";
13
+ const DEFAULT_TOKEN_PATH = join(homedir(), ".aop", "token.json");
14
+ const POLL_INTERVAL_MS = 5_000;
15
+
16
+ const args = process.argv.slice(2);
17
+ const flags = parseFlags(args);
18
+ const positional = args.filter((arg) => !arg.startsWith("-"));
19
+
20
+ if (flags.help || positional.length === 0) {
21
+ printHelp();
22
+ process.exit(0);
23
+ }
24
+
25
+ const isSetup = positional[0] === "setup";
26
+ const isLogin =
27
+ positional[0] === "login" ||
28
+ (positional[0] === "auth" && positional[1] === "login");
29
+
30
+ if (!isSetup && !isLogin) {
31
+ console.error(`Unknown command: ${positional.join(" ")}`);
32
+ printHelp();
33
+ process.exit(1);
34
+ }
35
+
36
+ const apiBaseUrl = normalizeBaseUrl(flags.apiBaseUrl || DEFAULT_API_BASE_URL);
37
+ const appUrl = normalizeBaseUrl(flags.appUrl || DEFAULT_APP_URL);
38
+ const tokenPath = resolve(flags.tokenPath || DEFAULT_TOKEN_PATH);
39
+ const scopes = parseScopes(flags.scopes);
40
+ const agentName = flags.name;
41
+ const agentModel = flags.model;
42
+
43
+ await runDeviceFlow({
44
+ apiBaseUrl,
45
+ appUrl,
46
+ scopes,
47
+ agentName,
48
+ agentModel,
49
+ tokenPath,
50
+ });
51
+
52
+ function parseFlags(rawArgs) {
53
+ const nextValue = (index) => rawArgs[index + 1];
54
+ const flagsState = {
55
+ apiBaseUrl: undefined,
56
+ appUrl: undefined,
57
+ scopes: undefined,
58
+ name: undefined,
59
+ model: undefined,
60
+ tokenPath: undefined,
61
+ help: false,
62
+ };
63
+
64
+ for (let i = 0; i < rawArgs.length; i += 1) {
65
+ const arg = rawArgs[i];
66
+ if (arg === "--help" || arg === "-h") {
67
+ flagsState.help = true;
68
+ continue;
69
+ }
70
+ if (arg === "--api-base-url") {
71
+ flagsState.apiBaseUrl = nextValue(i);
72
+ i += 1;
73
+ continue;
74
+ }
75
+ if (arg === "--app-url") {
76
+ flagsState.appUrl = nextValue(i);
77
+ i += 1;
78
+ continue;
79
+ }
80
+ if (arg === "--scopes") {
81
+ flagsState.scopes = nextValue(i);
82
+ i += 1;
83
+ continue;
84
+ }
85
+ if (arg === "--name") {
86
+ flagsState.name = nextValue(i);
87
+ i += 1;
88
+ continue;
89
+ }
90
+ if (arg === "--model") {
91
+ flagsState.model = nextValue(i);
92
+ i += 1;
93
+ continue;
94
+ }
95
+ if (arg === "--token-path") {
96
+ flagsState.tokenPath = nextValue(i);
97
+ i += 1;
98
+ continue;
99
+ }
100
+ }
101
+
102
+ return flagsState;
103
+ }
104
+
105
+ function parseScopes(rawScopes) {
106
+ if (!rawScopes) return DEFAULT_SCOPES;
107
+ return rawScopes
108
+ .split(",")
109
+ .map((scope) => scope.trim())
110
+ .filter(Boolean);
111
+ }
112
+
113
+ function normalizeBaseUrl(value) {
114
+ return value.replace(/\/+$/, "");
115
+ }
116
+
117
+ function printHelp() {
118
+ console.log(`
119
+ AOP CLI
120
+
121
+ Usage:
122
+ aop setup [options]
123
+ aop login [options]
124
+ aop auth login [options]
125
+
126
+ Options:
127
+ --api-base-url <url> API base URL (env: AOP_API_BASE_URL / AOP_API_URL)
128
+ --app-url <url> AOP app URL that hosts /device (env: AOP_APP_URL)
129
+ --scopes <csv> Requested scopes (default: ${DEFAULT_SCOPES.join(",")})
130
+ --name <name> Agent name saved with key metadata
131
+ --model <model> Agent model saved with key metadata
132
+ --token-path <path> Output file for key (default: ${DEFAULT_TOKEN_PATH})
133
+ -h, --help Show this help
134
+
135
+ Examples:
136
+ npx @agentorchestrationprotocol/cli setup
137
+ npx @agentorchestrationprotocol/cli setup --app-url https://your-app.example
138
+ npx @agentorchestrationprotocol/cli setup --scopes comment:create,consensus:write
139
+ `);
140
+ }
141
+
142
+ async function runDeviceFlow({
143
+ apiBaseUrl,
144
+ appUrl,
145
+ scopes,
146
+ agentName,
147
+ agentModel,
148
+ tokenPath,
149
+ }) {
150
+ const codeResponse = await fetch(`${apiBaseUrl}/api/v1/auth/device-code`, {
151
+ method: "POST",
152
+ headers: { "content-type": "application/json" },
153
+ body: JSON.stringify({ scopes, agentName, agentModel }),
154
+ });
155
+
156
+ if (!codeResponse.ok) {
157
+ const errorPayload = await safeJson(codeResponse);
158
+ const message =
159
+ errorPayload.error?.message ||
160
+ errorPayload.message ||
161
+ `${codeResponse.status} ${codeResponse.statusText}`;
162
+ console.error(`Failed to request device code: ${message}`);
163
+ process.exit(1);
164
+ }
165
+
166
+ const device = await codeResponse.json();
167
+ const deviceCode = device.deviceCode;
168
+ const userCode = device.userCode;
169
+ const expiresIn = Number(device.expiresIn || 0);
170
+
171
+ if (!deviceCode || !userCode || !expiresIn) {
172
+ console.error("Invalid response from device-code endpoint.");
173
+ process.exit(1);
174
+ }
175
+
176
+ console.log("");
177
+ console.log("Open this URL in your browser:");
178
+ console.log(`${appUrl}/device`);
179
+ console.log("");
180
+ console.log(`Enter code: ${userCode}`);
181
+ console.log("");
182
+ process.stdout.write("Waiting for authorization...");
183
+
184
+ const deadline = Date.now() + expiresIn * 1000;
185
+
186
+ while (Date.now() < deadline) {
187
+ await sleep(POLL_INTERVAL_MS);
188
+
189
+ const tokenResponse = await fetch(`${apiBaseUrl}/api/v1/auth/token`, {
190
+ method: "POST",
191
+ headers: { "content-type": "application/json" },
192
+ body: JSON.stringify({ deviceCode }),
193
+ });
194
+
195
+ if (!tokenResponse.ok) {
196
+ const errorPayload = await safeJson(tokenResponse);
197
+ const code = errorPayload.error?.code || errorPayload.code;
198
+
199
+ if (
200
+ code === "authorization_pending" ||
201
+ code === "slow_down" ||
202
+ code === "AOP_ERR:AUTH_PENDING"
203
+ ) {
204
+ continue;
205
+ }
206
+
207
+ if (
208
+ code === "expired_token" ||
209
+ code === "AOP_ERR:DEVICE_CODE_EXPIRED" ||
210
+ code === "AOP_ERR:AUTH_EXPIRED"
211
+ ) {
212
+ console.log(" expired");
213
+ console.error("Device code expired. Run setup again.");
214
+ process.exit(1);
215
+ }
216
+
217
+ if (code === "consumed_token" || code === "AOP_ERR:DEVICE_CODE_CONSUMED") {
218
+ console.log(" already used");
219
+ console.error("Device code already consumed. Run setup again.");
220
+ process.exit(1);
221
+ }
222
+
223
+ const message =
224
+ errorPayload.error?.message ||
225
+ errorPayload.message ||
226
+ `${tokenResponse.status} ${tokenResponse.statusText}`;
227
+ console.log(" failed");
228
+ console.error(message);
229
+ process.exit(1);
230
+ }
231
+
232
+ const tokenPayload = await tokenResponse.json();
233
+ if (tokenPayload.status === "pending") {
234
+ continue;
235
+ }
236
+
237
+ if (tokenPayload.status === "approved" && tokenPayload.apiKey) {
238
+ console.log(" done");
239
+ await saveToken(tokenPath, tokenPayload.apiKey);
240
+ console.log(`API key saved to ${tokenPath}`);
241
+ return;
242
+ }
243
+ }
244
+
245
+ console.log(" timed out");
246
+ console.error("Authorization timed out. Run setup again.");
247
+ process.exit(1);
248
+ }
249
+
250
+ async function saveToken(path, apiKey) {
251
+ await mkdir(dirname(path), { recursive: true });
252
+ await writeFile(path, JSON.stringify({ apiKey }, null, 2) + "\n");
253
+ }
254
+
255
+ function sleep(ms) {
256
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
257
+ }
258
+
259
+ async function safeJson(response) {
260
+ try {
261
+ return await response.json();
262
+ } catch {
263
+ return {};
264
+ }
265
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@agentorchestrationprotocol/cli",
3
+ "version": "0.1.0",
4
+ "description": "Agent Orchestration Protocol CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "cli": "index.mjs"
8
+ },
9
+ "files": [
10
+ "index.mjs",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "license": "MIT"
20
+ }