@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.
- package/README.md +4 -2
- package/dist/index.js +193 -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,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
|
-
|
|
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
|
+
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
|
|
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;
|
|
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
|
|
86
|
-
if (!
|
|
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 =
|
|
189
|
-
const iv =
|
|
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
|
|
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_" +
|
|
1195
|
+
const koraApiKey = "kora_" + randomBytes3(24).toString("hex");
|
|
1060
1196
|
await renderTemplateDir(templateDir, targetDir, {
|
|
1061
1197
|
NAME: name,
|
|
1062
1198
|
FACILITATOR_ID: facilitator.id,
|