@agentis-hq/cli 0.1.7 → 0.3.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 +4 -2
  2. package/dist/index.js +235 -72
  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,230 @@ 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
+ const timeout = new Promise((_, reject) => {
169
+ setTimeout(() => reject(new Error("Login timed out. Run `agentis login` again.")), 10 * 60 * 1e3);
170
+ });
56
171
  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;
172
+ const result = await Promise.race([callback, timeout]);
173
+ if (result.error) throw new Error(`Authorization failed: ${result.error}`);
174
+ if (!result.code || result.state !== state) throw new Error("Invalid OAuth callback");
175
+ const response = await fetch(`${API_BASE}/oauth/token`, {
176
+ method: "POST",
177
+ headers: { "content-type": "application/x-www-form-urlencoded" },
178
+ body: new URLSearchParams({
179
+ grant_type: "authorization_code",
180
+ code: result.code,
181
+ redirect_uri: redirectUri,
182
+ code_verifier: verifier,
183
+ client_id: CLIENT_ID
184
+ })
185
+ });
186
+ const body = await response.json();
187
+ if (!response.ok || !body.access_token || !body.refresh_token || !body.expires_in) {
188
+ throw new Error(body.error_description ?? "OAuth token exchange failed");
79
189
  }
190
+ await saveOAuthCredentials({
191
+ version: 2,
192
+ accessToken: body.access_token,
193
+ refreshToken: body.refresh_token,
194
+ expiresAt: Date.now() + body.expires_in * 1e3,
195
+ scope: body.scope?.split(/\s+/).filter(Boolean) ?? SCOPES,
196
+ clientId: CLIENT_ID
197
+ });
198
+ console.log("\nAuthenticated. You can now use the Agentis CLI.\n");
199
+ } catch (error) {
200
+ console.error(`
201
+ ${error instanceof Error ? error.message : "Login failed"}`);
202
+ process.exitCode = 1;
203
+ } finally {
204
+ server.stop(true);
80
205
  }
81
- console.error("\nLogin timed out. Run `agentis login` again.");
82
- process.exit(1);
83
206
  }
84
207
  async function logout() {
85
- const token = await getToken();
86
- if (!token) {
208
+ const stored = await getStoredCredentials();
209
+ if (!stored) {
87
210
  console.log("Not logged in.");
88
211
  return;
89
212
  }
213
+ if (stored.type === "oauth") {
214
+ await fetch(`${API_BASE}/oauth/revoke`, {
215
+ method: "POST",
216
+ headers: { "content-type": "application/x-www-form-urlencoded" },
217
+ body: new URLSearchParams({ token: stored.credentials.refreshToken })
218
+ }).catch(() => null);
219
+ }
90
220
  await deleteToken();
91
221
  console.log("Logged out.");
92
222
  }
93
223
  async function whoami() {
224
+ const stored = await getStoredCredentials();
94
225
  const token = await getToken();
95
- if (!token) {
226
+ if (!stored || !token) {
96
227
  console.log("Not logged in. Run `agentis login`.");
97
228
  return;
98
229
  }
99
230
  const masked = token.slice(0, 13) + "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + token.slice(-4);
100
- console.log(`Logged in as ${masked}`);
231
+ console.log(`Logged in via ${stored.type === "oauth" ? "OAuth" : "account key"} as ${masked}`);
101
232
  }
102
233
 
103
234
  // src/lib/account.ts
@@ -134,7 +265,7 @@ import { wordlist as englishWordlist } from "@scure/bip39/wordlists/english.js";
134
265
  import { HDKey } from "@scure/bip32";
135
266
  import { scrypt } from "@noble/hashes/scrypt.js";
136
267
  import { gcm } from "@noble/ciphers/aes.js";
137
- import { randomBytes } from "@noble/hashes/utils.js";
268
+ import { randomBytes as randomBytes2 } from "@noble/hashes/utils.js";
138
269
  import { ed25519 } from "@noble/curves/ed25519.js";
139
270
  import { v4 as uuidv4 } from "uuid";
140
271
  import { join } from "path";
@@ -185,8 +316,8 @@ function encodeBase58(bytes) {
185
316
  return result;
186
317
  }
187
318
  function encryptMnemonic(mnemonic, passphrase = "") {
188
- const salt = randomBytes(32);
189
- const iv = randomBytes(12);
319
+ const salt = randomBytes2(32);
320
+ const iv = randomBytes2(12);
190
321
  const key = scrypt(passphrase, salt, { N: 65536, r: 8, p: 1, dkLen: 32 });
191
322
  const cipher = gcm(key, iv);
192
323
  const data = new TextEncoder().encode(mnemonic);
@@ -489,6 +620,13 @@ Sent!`);
489
620
  }
490
621
 
491
622
  // src/lib/format-agent.ts
623
+ var blue = "\x1B[38;5;117m";
624
+ var purple = "\x1B[38;5;141m";
625
+ var green = "\x1B[38;5;114m";
626
+ var amber = "\x1B[38;5;179m";
627
+ var red = "\x1B[38;5;203m";
628
+ var muted = "\x1B[38;5;244m";
629
+ var reset = "\x1B[0m";
492
630
  function formatPolicy(agent) {
493
631
  const mode = agent.policyMode ?? "backend";
494
632
  if (mode !== "onchain") return "policy=backend";
@@ -501,16 +639,41 @@ function formatPrivacy(agent) {
501
639
  function formatLocalPolicy(wallet) {
502
640
  return wallet.policy.killSwitch ? "policy=local:killed" : "policy=local";
503
641
  }
642
+ function shorten(value, prefix = 6, suffix = 5) {
643
+ if (value.length <= prefix + suffix + 3) return value;
644
+ return `${value.slice(0, prefix)}...${value.slice(-suffix)}`;
645
+ }
646
+ function color(value, ansi) {
647
+ return `${ansi}${value}${reset}`;
648
+ }
649
+ function colorPolicy(policy) {
650
+ if (policy === "policy=onchain:ready") return color(policy, purple);
651
+ if (policy === "policy=onchain:pending") return color(policy, amber);
652
+ if (policy === "policy=local:killed") return color(policy, red);
653
+ return color(policy, muted);
654
+ }
655
+ function colorPrivacy(privacy) {
656
+ if (!privacy) return null;
657
+ if (privacy === "privacy=registered") return color(privacy, green);
658
+ if (privacy === "privacy=pending") return color(privacy, amber);
659
+ if (privacy === "privacy=failed") return color(privacy, red);
660
+ return color(privacy, muted);
661
+ }
504
662
  function formatBadges(parts) {
505
- return parts.filter(Boolean).join(", ");
663
+ return parts.filter(Boolean).join(" ");
664
+ }
665
+ function formatName(name) {
666
+ return color(shorten(name, 17, 4).padEnd(24), blue);
506
667
  }
507
668
  function formatHostedAgentLine(agent) {
508
- const badges = formatBadges(["hosted", formatPolicy(agent), formatPrivacy(agent)]);
509
- return ` ${agent.name.padEnd(20)} ${agent.walletAddress} [${agent.id}] ${badges}`;
669
+ const badges = formatBadges([
670
+ colorPolicy(formatPolicy(agent)),
671
+ colorPrivacy(formatPrivacy(agent))
672
+ ]);
673
+ return ` ${formatName(agent.name)} ${shorten(agent.walletAddress)} ${badges}`;
510
674
  }
511
675
  function formatLocalWalletLine(wallet) {
512
- const badges = formatBadges(["local", formatLocalPolicy(wallet)]);
513
- return ` ${wallet.name.padEnd(20)} ${wallet.solanaAddress} [${wallet.id}] ${badges}`;
676
+ return ` ${formatName(wallet.name)} ${shorten(wallet.solanaAddress)} ${colorPolicy(formatLocalPolicy(wallet))}`;
514
677
  }
515
678
 
516
679
  // src/commands/wallet.ts
@@ -905,7 +1068,7 @@ async function privacyCommand(args2) {
905
1068
  import { mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
906
1069
  import { existsSync as existsSync2 } from "fs";
907
1070
  import { dirname, join as join2, resolve } from "path";
908
- import { randomBytes as randomBytes2 } from "crypto";
1071
+ import { randomBytes as randomBytes3 } from "crypto";
909
1072
  var DEVNET_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
910
1073
  var SOLANA_DEVNET_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
911
1074
  async function requireAuth2() {
@@ -1024,7 +1187,7 @@ async function facilitatorCreate(args2) {
1024
1187
  }
1025
1188
  const facilitator = await res.json();
1026
1189
  const templateDir = facilitatorTemplateDir();
1027
- const koraApiKey = "kora_" + randomBytes2(24).toString("hex");
1190
+ const koraApiKey = "kora_" + randomBytes3(24).toString("hex");
1028
1191
  await renderTemplateDir(templateDir, targetDir, {
1029
1192
  NAME: name,
1030
1193
  FACILITATOR_ID: facilitator.id,
@@ -1395,11 +1558,11 @@ async function earnSweep(args2) {
1395
1558
  var args = process.argv.slice(2);
1396
1559
  var cmd = args[0];
1397
1560
  var sub = args[1];
1398
- var blue = "\x1B[38;5;117m";
1399
- var green = "\x1B[38;5;114m";
1400
- var muted = "\x1B[38;5;244m";
1561
+ var blue2 = "\x1B[38;5;117m";
1562
+ var green2 = "\x1B[38;5;114m";
1563
+ var muted2 = "\x1B[38;5;244m";
1401
1564
  var bold = "\x1B[1m";
1402
- var reset = "\x1B[0m";
1565
+ var reset2 = "\x1B[0m";
1403
1566
  function readCliVersion() {
1404
1567
  try {
1405
1568
  const packageJsonPath = join3(dirname2(fileURLToPath(import.meta.url)), "..", "package.json");
@@ -1671,18 +1834,18 @@ var helpSpecs = {
1671
1834
  }
1672
1835
  };
1673
1836
  function showHelp() {
1674
- console.log(`${blue}${bold}
1837
+ console.log(`${blue2}${bold}
1675
1838
  \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
1676
1839
  \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
1677
1840
  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
1678
1841
  \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551
1679
1842
  \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
1680
1843
  \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1681
- ${reset}${muted}v${version}${reset}
1844
+ ${reset2}${muted2}v${version}${reset2}
1682
1845
 
1683
- ${bold}Agentis${reset} \u2014 financial infrastructure for AI agents
1846
+ ${bold}Agentis${reset2} \u2014 financial infrastructure for AI agents
1684
1847
 
1685
- ${green}${bold}Commands:${reset}
1848
+ ${green2}${bold}Commands:${reset2}
1686
1849
  login authenticate with your Agentis account
1687
1850
  logout remove stored credentials
1688
1851
  whoami show current account
@@ -1751,7 +1914,7 @@ function helpPath(values) {
1751
1914
  }
1752
1915
  function printRows(title, rows) {
1753
1916
  console.log(`
1754
- ${green}${bold}${title}:${reset}`);
1917
+ ${green2}${bold}${title}:${reset2}`);
1755
1918
  for (const [left, right] of rows) {
1756
1919
  console.log(` ${left.padEnd(38)} ${right}`);
1757
1920
  }
@@ -1766,7 +1929,7 @@ function showCommandHelp(path) {
1766
1929
  showHelp();
1767
1930
  return;
1768
1931
  }
1769
- console.log(`${bold}Usage:${reset} ${spec.usage}
1932
+ console.log(`${bold}Usage:${reset2} ${spec.usage}
1770
1933
  `);
1771
1934
  console.log(spec.description);
1772
1935
  if (spec.commands) printRows("Commands", spec.commands);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentis-hq/cli",
3
- "version": "0.1.7",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "agentis": "dist/index.js"