@agentis-hq/cli 0.2.0 → 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.
- package/README.md +4 -2
- package/dist/index.js +188 -57
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
return
|
|
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
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(` ${
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
86
|
-
if (!
|
|
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 =
|
|
189
|
-
const iv =
|
|
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);
|
|
@@ -937,7 +1068,7 @@ async function privacyCommand(args2) {
|
|
|
937
1068
|
import { mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
|
|
938
1069
|
import { existsSync as existsSync2 } from "fs";
|
|
939
1070
|
import { dirname, join as join2, resolve } from "path";
|
|
940
|
-
import { randomBytes as
|
|
1071
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
941
1072
|
var DEVNET_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
|
|
942
1073
|
var SOLANA_DEVNET_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
943
1074
|
async function requireAuth2() {
|
|
@@ -1056,7 +1187,7 @@ async function facilitatorCreate(args2) {
|
|
|
1056
1187
|
}
|
|
1057
1188
|
const facilitator = await res.json();
|
|
1058
1189
|
const templateDir = facilitatorTemplateDir();
|
|
1059
|
-
const koraApiKey = "kora_" +
|
|
1190
|
+
const koraApiKey = "kora_" + randomBytes3(24).toString("hex");
|
|
1060
1191
|
await renderTemplateDir(templateDir, targetDir, {
|
|
1061
1192
|
NAME: name,
|
|
1062
1193
|
FACILITATOR_ID: facilitator.id,
|