@agentis-hq/cli 0.2.0 → 0.3.1

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 +4 -2
  2. package/dist/index.js +193 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -43,7 +43,9 @@ AGENTIS_API_URL=http://localhost:3001 agentis wallet list
43
43
 
44
44
  ## Authentication
45
45
 
46
- Hosted wallets and hosted agents require an Agentis account key. The CLI gets one through the browser login flow and stores it in your OS keychain.
46
+ Hosted wallets and hosted agents require authentication. The CLI uses OAuth
47
+ authorization code flow with PKCE and stores its access and refresh credentials
48
+ in the OS keychain.
47
49
 
48
50
  ```bash
49
51
  agentis login
@@ -278,7 +280,7 @@ AGENTIS_API_URL=http://localhost:3001 bun src/index.ts wallet list
278
280
  ## Notes
279
281
 
280
282
  - Hosted agent keys are shown only when created or regenerated.
281
- - CLI account keys are stored in the OS keychain.
283
+ - CLI OAuth credentials are stored in the OS keychain.
282
284
  - Local wallet vaults live under `~/.agentis/wallets/`.
283
285
  - Jupiter Earn commands require mainnet and the `--mainnet` safety flag.
284
286
  - Umbra devnet flows are currently safest with SOL or wSOL.
package/dist/index.js CHANGED
@@ -5,99 +5,235 @@ import { readFileSync as readFileSync2 } from "fs";
5
5
  import { dirname as dirname2, join as join3 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
+ // src/commands/auth.ts
9
+ import { createHash, randomBytes } from "crypto";
10
+ import { execFile } from "child_process";
11
+
12
+ // src/lib/config.ts
13
+ var API_BASE = process.env.AGENTIS_API_URL ?? "https://api.agentis.systems";
14
+ async function apiFetch(path, opts = {}, token) {
15
+ const headers = {
16
+ "content-type": "application/json",
17
+ ...opts.headers ?? {}
18
+ };
19
+ if (token) headers["authorization"] = `Bearer ${token}`;
20
+ return fetch(`${API_BASE}${path}`, { ...opts, headers });
21
+ }
22
+
8
23
  // src/lib/keychain.ts
9
24
  import { Entry } from "@napi-rs/keyring";
10
25
  var entry = new Entry("agentis-cli", "account-key");
11
- async function saveToken(token) {
12
- entry.setPassword(token);
13
- }
14
- async function getToken() {
15
- if (process.env.AGENTIS_ACCOUNT_KEY) return process.env.AGENTIS_ACCOUNT_KEY;
26
+ function readPassword() {
16
27
  try {
17
28
  return entry.getPassword();
18
29
  } catch {
19
30
  return null;
20
31
  }
21
32
  }
22
- async function deleteToken() {
33
+ async function saveOAuthCredentials(credentials) {
34
+ entry.setPassword(JSON.stringify(credentials));
35
+ }
36
+ async function getStoredCredentials() {
37
+ const envToken = process.env.AGENTIS_ACCOUNT_KEY;
38
+ if (envToken) return { type: "legacy", token: envToken };
39
+ const stored = readPassword();
40
+ if (!stored) return null;
41
+ if (!stored.startsWith("{")) return { type: "legacy", token: stored };
23
42
  try {
24
- entry.deletePassword();
43
+ const credentials = JSON.parse(stored);
44
+ if (credentials.version === 2 && credentials.accessToken?.startsWith("agt_oauth_") && credentials.refreshToken?.startsWith("agt_refresh_")) {
45
+ return { type: "oauth", credentials };
46
+ }
25
47
  } catch {
26
48
  }
49
+ return null;
27
50
  }
28
-
29
- // src/lib/config.ts
30
- var API_BASE = process.env.AGENTIS_API_URL ?? "https://api.agentis.systems";
31
- async function apiFetch(path, opts = {}, token) {
32
- const headers = {
33
- "content-type": "application/json",
34
- ...opts.headers ?? {}
51
+ async function refresh(credentials) {
52
+ const response = await fetch(`${API_BASE}/oauth/token`, {
53
+ method: "POST",
54
+ headers: { "content-type": "application/x-www-form-urlencoded" },
55
+ body: new URLSearchParams({
56
+ grant_type: "refresh_token",
57
+ refresh_token: credentials.refreshToken,
58
+ client_id: credentials.clientId
59
+ })
60
+ }).catch(() => null);
61
+ if (!response?.ok) return null;
62
+ const body = await response.json();
63
+ const updated = {
64
+ ...credentials,
65
+ accessToken: body.access_token,
66
+ refreshToken: body.refresh_token,
67
+ expiresAt: Date.now() + body.expires_in * 1e3,
68
+ scope: body.scope.split(/\s+/).filter(Boolean)
35
69
  };
36
- if (token) headers["authorization"] = `Bearer ${token}`;
37
- return fetch(`${API_BASE}${path}`, { ...opts, headers });
70
+ await saveOAuthCredentials(updated);
71
+ return updated;
72
+ }
73
+ async function getToken() {
74
+ const stored = await getStoredCredentials();
75
+ if (!stored) return null;
76
+ if (stored.type === "legacy") return stored.token;
77
+ if (stored.credentials.expiresAt > Date.now() + 6e4) {
78
+ return stored.credentials.accessToken;
79
+ }
80
+ return (await refresh(stored.credentials))?.accessToken ?? null;
81
+ }
82
+ async function deleteToken() {
83
+ try {
84
+ entry.deletePassword();
85
+ } catch {
86
+ }
38
87
  }
39
88
 
40
89
  // src/commands/auth.ts
90
+ var CLIENT_ID = "agentis-cli";
91
+ var SCOPES = [
92
+ "wallets:read",
93
+ "wallets:write",
94
+ "payments:execute",
95
+ "policy:read",
96
+ "policy:write",
97
+ "privacy:read",
98
+ "privacy:write",
99
+ "earn:read",
100
+ "earn:write"
101
+ ];
102
+ function base64url(bytes) {
103
+ return Buffer.from(bytes).toString("base64url");
104
+ }
105
+ function openBrowser(url) {
106
+ if (process.platform === "darwin") {
107
+ execFile("open", [url], () => {
108
+ });
109
+ } else if (process.platform === "win32") {
110
+ execFile("cmd", ["/c", "start", "", url], () => {
111
+ });
112
+ } else {
113
+ execFile("xdg-open", [url], () => {
114
+ });
115
+ }
116
+ }
41
117
  async function login() {
42
- const existing = await getToken();
118
+ const existing = await getStoredCredentials();
43
119
  if (existing) {
44
120
  console.log("Already logged in. Run `agentis logout` first.");
45
121
  return;
46
122
  }
47
- const res = await apiFetch("/auth/session", { method: "POST" });
48
- if (!res.ok) {
49
- console.error("Failed to start login session.");
50
- process.exit(1);
51
- }
52
- const { sessionId, loginUrl } = await res.json();
123
+ const verifier = base64url(randomBytes(48));
124
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
125
+ const state = base64url(randomBytes(24));
126
+ let resolveCallback;
127
+ const callback = new Promise((resolve2) => {
128
+ resolveCallback = resolve2;
129
+ });
130
+ let handled = false;
131
+ const server = Bun.serve({
132
+ hostname: "127.0.0.1",
133
+ port: 0,
134
+ fetch(request) {
135
+ const url = new URL(request.url);
136
+ if (url.pathname !== "/callback") return new Response("Not found", { status: 404 });
137
+ if (!handled) {
138
+ handled = true;
139
+ resolveCallback({
140
+ code: url.searchParams.get("code") ?? void 0,
141
+ error: url.searchParams.get("error") ?? void 0,
142
+ state: url.searchParams.get("state") ?? void 0
143
+ });
144
+ }
145
+ return new Response(
146
+ '<!doctype html><html><body style="font-family:monospace;padding:40px">Agentis authorization complete. You can close this window.</body></html>',
147
+ { headers: { "content-type": "text/html; charset=utf-8" } }
148
+ );
149
+ }
150
+ });
151
+ const redirectUri = `http://127.0.0.1:${server.port}/callback`;
152
+ const authorizeUrl = new URL(`${API_BASE}/oauth/authorize`);
153
+ authorizeUrl.search = new URLSearchParams({
154
+ response_type: "code",
155
+ client_id: CLIENT_ID,
156
+ redirect_uri: redirectUri,
157
+ code_challenge: challenge,
158
+ code_challenge_method: "S256",
159
+ scope: SCOPES.join(" "),
160
+ state,
161
+ resource: API_BASE
162
+ }).toString();
53
163
  console.log("\nOpen this URL in your browser to authenticate:\n");
54
- console.log(` ${loginUrl}
164
+ console.log(` ${authorizeUrl}
55
165
  `);
166
+ openBrowser(authorizeUrl.toString());
167
+ console.log("Waiting for authorization...");
168
+ let timeoutId;
169
+ const timeout = new Promise((_, reject) => {
170
+ timeoutId = setTimeout(
171
+ () => reject(new Error("Login timed out. Run `agentis login` again.")),
172
+ 10 * 60 * 1e3
173
+ );
174
+ });
56
175
  try {
57
- const { exec } = await import("child_process");
58
- const open = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
59
- exec(`${open} "${loginUrl}"`);
60
- } catch {
61
- }
62
- console.log("Waiting for authentication...");
63
- const deadline = Date.now() + 10 * 60 * 1e3;
64
- while (Date.now() < deadline) {
65
- await new Promise((r) => setTimeout(r, 2e3));
66
- const poll = await apiFetch(`/auth/session/${sessionId}`);
67
- if (!poll.ok) {
68
- if (poll.status === 410) {
69
- console.error("\nSession expired. Run `agentis login` again.");
70
- process.exit(1);
71
- }
72
- continue;
73
- }
74
- const data = await poll.json();
75
- if (data.status === "complete" && data.accountKey) {
76
- await saveToken(data.accountKey);
77
- console.log("\nAuthenticated! You can now use the Agentis CLI.\n");
78
- return;
176
+ const result = await Promise.race([callback, timeout]);
177
+ if (result.error) throw new Error(`Authorization failed: ${result.error}`);
178
+ if (!result.code || result.state !== state) throw new Error("Invalid OAuth callback");
179
+ const response = await fetch(`${API_BASE}/oauth/token`, {
180
+ method: "POST",
181
+ headers: { "content-type": "application/x-www-form-urlencoded" },
182
+ body: new URLSearchParams({
183
+ grant_type: "authorization_code",
184
+ code: result.code,
185
+ redirect_uri: redirectUri,
186
+ code_verifier: verifier,
187
+ client_id: CLIENT_ID
188
+ })
189
+ });
190
+ const body = await response.json();
191
+ if (!response.ok || !body.access_token || !body.refresh_token || !body.expires_in) {
192
+ throw new Error(body.error_description ?? "OAuth token exchange failed");
79
193
  }
194
+ await saveOAuthCredentials({
195
+ version: 2,
196
+ accessToken: body.access_token,
197
+ refreshToken: body.refresh_token,
198
+ expiresAt: Date.now() + body.expires_in * 1e3,
199
+ scope: body.scope?.split(/\s+/).filter(Boolean) ?? SCOPES,
200
+ clientId: CLIENT_ID
201
+ });
202
+ console.log("\nAuthenticated. You can now use the Agentis CLI.\n");
203
+ } catch (error) {
204
+ console.error(`
205
+ ${error instanceof Error ? error.message : "Login failed"}`);
206
+ process.exitCode = 1;
207
+ } finally {
208
+ if (timeoutId) clearTimeout(timeoutId);
209
+ server.stop(true);
80
210
  }
81
- console.error("\nLogin timed out. Run `agentis login` again.");
82
- process.exit(1);
83
211
  }
84
212
  async function logout() {
85
- const token = await getToken();
86
- if (!token) {
213
+ const stored = await getStoredCredentials();
214
+ if (!stored) {
87
215
  console.log("Not logged in.");
88
216
  return;
89
217
  }
218
+ if (stored.type === "oauth") {
219
+ await fetch(`${API_BASE}/oauth/revoke`, {
220
+ method: "POST",
221
+ headers: { "content-type": "application/x-www-form-urlencoded" },
222
+ body: new URLSearchParams({ token: stored.credentials.refreshToken })
223
+ }).catch(() => null);
224
+ }
90
225
  await deleteToken();
91
226
  console.log("Logged out.");
92
227
  }
93
228
  async function whoami() {
229
+ const stored = await getStoredCredentials();
94
230
  const token = await getToken();
95
- if (!token) {
231
+ if (!stored || !token) {
96
232
  console.log("Not logged in. Run `agentis login`.");
97
233
  return;
98
234
  }
99
235
  const masked = token.slice(0, 13) + "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + token.slice(-4);
100
- console.log(`Logged in as ${masked}`);
236
+ console.log(`Logged in via ${stored.type === "oauth" ? "OAuth" : "account key"} as ${masked}`);
101
237
  }
102
238
 
103
239
  // src/lib/account.ts
@@ -134,7 +270,7 @@ import { wordlist as englishWordlist } from "@scure/bip39/wordlists/english.js";
134
270
  import { HDKey } from "@scure/bip32";
135
271
  import { scrypt } from "@noble/hashes/scrypt.js";
136
272
  import { gcm } from "@noble/ciphers/aes.js";
137
- import { randomBytes } from "@noble/hashes/utils.js";
273
+ import { randomBytes as randomBytes2 } from "@noble/hashes/utils.js";
138
274
  import { ed25519 } from "@noble/curves/ed25519.js";
139
275
  import { v4 as uuidv4 } from "uuid";
140
276
  import { join } from "path";
@@ -185,8 +321,8 @@ function encodeBase58(bytes) {
185
321
  return result;
186
322
  }
187
323
  function encryptMnemonic(mnemonic, passphrase = "") {
188
- const salt = randomBytes(32);
189
- const iv = randomBytes(12);
324
+ const salt = randomBytes2(32);
325
+ const iv = randomBytes2(12);
190
326
  const key = scrypt(passphrase, salt, { N: 65536, r: 8, p: 1, dkLen: 32 });
191
327
  const cipher = gcm(key, iv);
192
328
  const data = new TextEncoder().encode(mnemonic);
@@ -937,7 +1073,7 @@ async function privacyCommand(args2) {
937
1073
  import { mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
938
1074
  import { existsSync as existsSync2 } from "fs";
939
1075
  import { dirname, join as join2, resolve } from "path";
940
- import { randomBytes as randomBytes2 } from "crypto";
1076
+ import { randomBytes as randomBytes3 } from "crypto";
941
1077
  var DEVNET_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
942
1078
  var SOLANA_DEVNET_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
943
1079
  async function requireAuth2() {
@@ -1056,7 +1192,7 @@ async function facilitatorCreate(args2) {
1056
1192
  }
1057
1193
  const facilitator = await res.json();
1058
1194
  const templateDir = facilitatorTemplateDir();
1059
- const koraApiKey = "kora_" + randomBytes2(24).toString("hex");
1195
+ const koraApiKey = "kora_" + randomBytes3(24).toString("hex");
1060
1196
  await renderTemplateDir(templateDir, targetDir, {
1061
1197
  NAME: name,
1062
1198
  FACILITATOR_ID: facilitator.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentis-hq/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "agentis": "dist/index.js"