@agentchainlabsai/cli 0.0.0-stage → 0.2.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 +215 -2
- package/dist/args.d.ts +24 -0
- package/dist/args.js +185 -0
- package/dist/auth.d.ts +64 -0
- package/dist/auth.js +248 -0
- package/dist/browser-pow.d.ts +9 -0
- package/dist/browser-pow.js +30 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +3840 -0
- package/dist/media.d.ts +21 -0
- package/dist/media.js +64 -0
- package/dist/wake.d.ts +83 -0
- package/dist/wake.js +241 -0
- package/dist/watch.d.ts +9 -0
- package/dist/watch.js +67 -0
- package/dist/webhook.d.ts +13 -0
- package/dist/webhook.js +53 -0
- package/package.json +34 -4
package/dist/auth.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI credential store + EVM wallet helpers for headless auth.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
5
|
+
import { mkdir, readFile, writeFile, chmod, copyFile } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { Wallet } from 'ethers';
|
|
9
|
+
import { loginWithSigner, registerWithSigner, requestChallenge, } from '@agentchainlabsai/sdk';
|
|
10
|
+
import { parseBrowserPowChallengeResponse, solveBrowserPowFromFields, } from './browser-pow.js';
|
|
11
|
+
const CRED_DIR = join(homedir(), '.agentchain');
|
|
12
|
+
const CRED_FILE = join(CRED_DIR, 'credentials.json');
|
|
13
|
+
export async function loadCredentials() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(CRED_FILE, 'utf8');
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function saveCredentials(creds) {
|
|
23
|
+
await mkdir(CRED_DIR, { recursive: true, mode: 0o700 });
|
|
24
|
+
await writeFile(CRED_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
25
|
+
try {
|
|
26
|
+
await chmod(CRED_FILE, 0o600);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* windows */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Repeat-onboarding guard: `connect` must not silently replace a working identity (and its only
|
|
34
|
+
* copy of the API key). Returns a refusal message when credentials already hold an identity,
|
|
35
|
+
* unless the caller passed --new or this is a re-login of the same wallet.
|
|
36
|
+
*/
|
|
37
|
+
export function connectRefusalForExistingCredentials(existing, opts) {
|
|
38
|
+
if (opts.forceNew)
|
|
39
|
+
return null;
|
|
40
|
+
const hasStoredIdentity = Boolean(existing && (existing.apiKey || existing.agentToken || existing.agentId));
|
|
41
|
+
if (!hasStoredIdentity && !opts.envApiKey)
|
|
42
|
+
return null;
|
|
43
|
+
// Wallet connect is a login for a known wallet (same wallet → same account server-side); it only
|
|
44
|
+
// must not overwrite a stored identity that belongs to a different wallet / browser passport.
|
|
45
|
+
if (opts.walletAddress && !hasStoredIdentity)
|
|
46
|
+
return null;
|
|
47
|
+
if (opts.walletAddress &&
|
|
48
|
+
existing?.walletAddress &&
|
|
49
|
+
existing.walletAddress.toLowerCase() === opts.walletAddress.toLowerCase()) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const who = existing?.agentId ? ` (agent ${existing.agentId})` : '';
|
|
53
|
+
const source = hasStoredIdentity ? `~/.agentchain/credentials.json${who}` : 'AGENTCHAIN_API_KEY / --api-key';
|
|
54
|
+
return (`Already connected: ${source} holds an agent identity. ` +
|
|
55
|
+
'Run `agentchain me` to use it. Pass --new to create a separate agent ' +
|
|
56
|
+
'(the current credentials file is backed up first).');
|
|
57
|
+
}
|
|
58
|
+
/** Copy the current credentials file aside before an intentional overwrite (--new). */
|
|
59
|
+
export async function backupCredentialsFile() {
|
|
60
|
+
const existing = await loadCredentials();
|
|
61
|
+
if (!existing)
|
|
62
|
+
return null;
|
|
63
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
64
|
+
const target = join(CRED_DIR, `credentials.backup-${stamp}.json`);
|
|
65
|
+
await copyFile(CRED_FILE, target);
|
|
66
|
+
try {
|
|
67
|
+
await chmod(target, 0o600);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* windows */
|
|
71
|
+
}
|
|
72
|
+
return target;
|
|
73
|
+
}
|
|
74
|
+
export function createEvmWallet() {
|
|
75
|
+
const w = Wallet.createRandom();
|
|
76
|
+
return { address: w.address, privateKey: w.privateKey };
|
|
77
|
+
}
|
|
78
|
+
export function walletFromPrivateKey(privateKey) {
|
|
79
|
+
return new Wallet(privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`);
|
|
80
|
+
}
|
|
81
|
+
export async function signChallengeMessage(privateKey, message) {
|
|
82
|
+
const wallet = walletFromPrivateKey(privateKey);
|
|
83
|
+
return wallet.signMessage(message);
|
|
84
|
+
}
|
|
85
|
+
export async function runAuthChallenge(baseUrl, walletAddress) {
|
|
86
|
+
return requestChallenge(baseUrl, walletAddress);
|
|
87
|
+
}
|
|
88
|
+
export async function runAuthRegister(baseUrl, privateKey, opts) {
|
|
89
|
+
const wallet = walletFromPrivateKey(privateKey);
|
|
90
|
+
const result = await registerWithSigner(baseUrl, wallet.address, (msg) => wallet.signMessage(msg), { name: opts.name, agreedToTerms: opts.agreedToTerms });
|
|
91
|
+
const existing = await loadCredentials();
|
|
92
|
+
await saveCredentials({
|
|
93
|
+
baseUrl,
|
|
94
|
+
walletAddress: wallet.address,
|
|
95
|
+
apiKey: result.apiKey ?? existing?.apiKey,
|
|
96
|
+
apiKeyExpiresAt: existing?.apiKeyExpiresAt,
|
|
97
|
+
agentId: result.agentId,
|
|
98
|
+
did: result.did,
|
|
99
|
+
updatedAt: new Date().toISOString(),
|
|
100
|
+
});
|
|
101
|
+
return { ...result, saved: Boolean(result.apiKey ?? existing?.apiKey) };
|
|
102
|
+
}
|
|
103
|
+
export async function runAuthLogin(baseUrl, privateKey, mode, opts) {
|
|
104
|
+
const wallet = walletFromPrivateKey(privateKey);
|
|
105
|
+
if (mode === 'register') {
|
|
106
|
+
if (!opts?.agreedToTerms) {
|
|
107
|
+
throw new Error('Registration requires explicit Terms and Privacy acceptance');
|
|
108
|
+
}
|
|
109
|
+
return runAuthRegister(baseUrl, privateKey, { agreedToTerms: true });
|
|
110
|
+
}
|
|
111
|
+
const result = await loginWithSigner(baseUrl, wallet.address, (msg) => wallet.signMessage(msg));
|
|
112
|
+
await saveCredentials({
|
|
113
|
+
baseUrl,
|
|
114
|
+
walletAddress: wallet.address,
|
|
115
|
+
agentToken: result.agentToken,
|
|
116
|
+
agentId: result.agentId,
|
|
117
|
+
did: result.did,
|
|
118
|
+
updatedAt: new Date().toISOString(),
|
|
119
|
+
});
|
|
120
|
+
return { ...result, saved: true };
|
|
121
|
+
}
|
|
122
|
+
/** Fingerprint for display (never log full key) */
|
|
123
|
+
export function keyFingerprint(key) {
|
|
124
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 8);
|
|
125
|
+
}
|
|
126
|
+
export function generateLocalSecret() {
|
|
127
|
+
return randomBytes(32).toString('hex');
|
|
128
|
+
}
|
|
129
|
+
async function readJsonResponse(response, operation) {
|
|
130
|
+
const raw = await response.text();
|
|
131
|
+
if (!raw.trim()) {
|
|
132
|
+
throw new Error(`${operation} failed: HTTP ${response.status} returned an empty response`);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(raw);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new Error(`${operation} failed: HTTP ${response.status} returned invalid JSON`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function runIdentityBrowserConnect(baseUrl, opts) {
|
|
142
|
+
const origin = baseUrl.replace(/\/$/, '');
|
|
143
|
+
const challengeRes = await fetch(`${origin}/api/v1/identity/connect/challenge`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'Content-Type': 'application/json' },
|
|
146
|
+
body: JSON.stringify({ method: 'browser' }),
|
|
147
|
+
});
|
|
148
|
+
const challengeJson = await readJsonResponse(challengeRes, 'Identity challenge');
|
|
149
|
+
if (!challengeRes.ok) {
|
|
150
|
+
throw new Error(challengeJson.error ?? `Identity challenge failed: HTTP ${challengeRes.status}`);
|
|
151
|
+
}
|
|
152
|
+
const powFields = parseBrowserPowChallengeResponse(challengeJson);
|
|
153
|
+
const challengeToken = typeof challengeJson.challengeToken === 'string' ? challengeJson.challengeToken : '';
|
|
154
|
+
if (!challengeToken) {
|
|
155
|
+
throw new Error('Challenge response missing challengeToken');
|
|
156
|
+
}
|
|
157
|
+
const solution = solveBrowserPowFromFields(powFields);
|
|
158
|
+
if (!solution)
|
|
159
|
+
throw new Error('PoW solve failed');
|
|
160
|
+
const body = {
|
|
161
|
+
method: 'browser',
|
|
162
|
+
challengeToken,
|
|
163
|
+
solution,
|
|
164
|
+
agreedToTerms: opts.agreedToTerms,
|
|
165
|
+
};
|
|
166
|
+
if (opts?.from)
|
|
167
|
+
body.from = opts.from;
|
|
168
|
+
if (opts?.cardUrl)
|
|
169
|
+
body.cardUrl = opts.cardUrl;
|
|
170
|
+
const connectRes = await fetch(`${origin}/api/v1/identity/connect`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify(body),
|
|
174
|
+
});
|
|
175
|
+
const data = await readJsonResponse(connectRes, 'Identity connect');
|
|
176
|
+
if (!connectRes.ok) {
|
|
177
|
+
throw new Error(data.error ?? `Identity connect failed: HTTP ${connectRes.status}`);
|
|
178
|
+
}
|
|
179
|
+
const apiKey = typeof data.apiKey === 'string' ? data.apiKey : undefined;
|
|
180
|
+
const apiKeyExpiresAt = typeof data.apiKeyExpiresAt === 'string' ? data.apiKeyExpiresAt : undefined;
|
|
181
|
+
const agentId = typeof data.agentId === 'string' ? data.agentId : undefined;
|
|
182
|
+
const did = typeof data.did === 'string' ? data.did : undefined;
|
|
183
|
+
if (apiKey && agentId) {
|
|
184
|
+
await saveCredentials({
|
|
185
|
+
baseUrl: origin,
|
|
186
|
+
walletAddress: '',
|
|
187
|
+
apiKey,
|
|
188
|
+
apiKeyExpiresAt,
|
|
189
|
+
agentId,
|
|
190
|
+
did,
|
|
191
|
+
updatedAt: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return { ...data, saved: Boolean(apiKey) };
|
|
195
|
+
}
|
|
196
|
+
export async function runIdentityWalletConnect(baseUrl, privateKey, opts) {
|
|
197
|
+
const wallet = walletFromPrivateKey(privateKey);
|
|
198
|
+
const origin = baseUrl.replace(/\/$/, '');
|
|
199
|
+
const challengeRes = await fetch(`${origin}/api/v1/identity/connect/challenge`, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: { 'Content-Type': 'application/json' },
|
|
202
|
+
body: JSON.stringify({ method: 'wallet', address: wallet.address }),
|
|
203
|
+
});
|
|
204
|
+
const challengeData = await readJsonResponse(challengeRes, 'Wallet challenge');
|
|
205
|
+
if (!challengeRes.ok) {
|
|
206
|
+
throw new Error(challengeData.error ?? `Wallet challenge failed: HTTP ${challengeRes.status}`);
|
|
207
|
+
}
|
|
208
|
+
const challenge = challengeData;
|
|
209
|
+
const signature = await wallet.signMessage(challenge.message);
|
|
210
|
+
const body = {
|
|
211
|
+
method: 'wallet',
|
|
212
|
+
address: wallet.address,
|
|
213
|
+
challengeToken: challenge.challengeToken,
|
|
214
|
+
signature,
|
|
215
|
+
agreedToTerms: opts.agreedToTerms,
|
|
216
|
+
};
|
|
217
|
+
if (opts.name)
|
|
218
|
+
body.name = opts.name;
|
|
219
|
+
if (opts.from)
|
|
220
|
+
body.from = opts.from;
|
|
221
|
+
if (opts.cardUrl)
|
|
222
|
+
body.cardUrl = opts.cardUrl;
|
|
223
|
+
const connectRes = await fetch(`${origin}/api/v1/identity/connect`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify(body),
|
|
227
|
+
});
|
|
228
|
+
const data = await readJsonResponse(connectRes, 'Wallet connect');
|
|
229
|
+
if (!connectRes.ok) {
|
|
230
|
+
throw new Error(data.error ?? `Wallet connect failed: HTTP ${connectRes.status}`);
|
|
231
|
+
}
|
|
232
|
+
const apiKey = typeof data.apiKey === 'string' ? data.apiKey : undefined;
|
|
233
|
+
const apiKeyExpiresAt = typeof data.apiKeyExpiresAt === 'string' ? data.apiKeyExpiresAt : undefined;
|
|
234
|
+
const agentId = typeof data.agentId === 'string' ? data.agentId : undefined;
|
|
235
|
+
const did = typeof data.did === 'string' ? data.did : undefined;
|
|
236
|
+
if (apiKey) {
|
|
237
|
+
await saveCredentials({
|
|
238
|
+
baseUrl: origin,
|
|
239
|
+
walletAddress: wallet.address,
|
|
240
|
+
apiKey,
|
|
241
|
+
apiKeyExpiresAt,
|
|
242
|
+
agentId,
|
|
243
|
+
did,
|
|
244
|
+
updatedAt: new Date().toISOString(),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return { ...data, saved: Boolean(apiKey) };
|
|
248
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay Connect browser PoW — mirrors lib/identity/browser-pow-client.ts for the CLI package.
|
|
3
|
+
*/
|
|
4
|
+
export type BrowserPowChallengeFields = {
|
|
5
|
+
nonce: string;
|
|
6
|
+
difficulty: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function solveBrowserPowFromFields(input: BrowserPowChallengeFields): string | null;
|
|
9
|
+
export declare function parseBrowserPowChallengeResponse(body: unknown): BrowserPowChallengeFields;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay Connect browser PoW — mirrors lib/identity/browser-pow-client.ts for the CLI package.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
const MAX_ITERATIONS = 5_000_000;
|
|
6
|
+
export function solveBrowserPowFromFields(input) {
|
|
7
|
+
const prefix = '0'.repeat(input.difficulty);
|
|
8
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
9
|
+
const candidate = String(i);
|
|
10
|
+
const hash = createHash('sha256').update(`${input.nonce}:${candidate}`).digest('hex');
|
|
11
|
+
if (hash.startsWith(prefix))
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
export function parseBrowserPowChallengeResponse(body) {
|
|
17
|
+
if (!body || typeof body !== 'object') {
|
|
18
|
+
throw new Error('Invalid challenge response from POST /api/v1/identity/connect/challenge');
|
|
19
|
+
}
|
|
20
|
+
const record = body;
|
|
21
|
+
const nonce = record.nonce;
|
|
22
|
+
const difficulty = record.difficulty;
|
|
23
|
+
if (typeof nonce !== 'string' || !nonce.trim()) {
|
|
24
|
+
throw new Error('Challenge response missing nonce — use method: browser on connect/challenge');
|
|
25
|
+
}
|
|
26
|
+
if (typeof difficulty !== 'number' || !Number.isFinite(difficulty)) {
|
|
27
|
+
throw new Error('Challenge response missing difficulty — use method: browser on connect/challenge');
|
|
28
|
+
}
|
|
29
|
+
return { nonce, difficulty };
|
|
30
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentChain CLI — Agent API v1 (same routes as OpenAPI + MCP).
|
|
4
|
+
*
|
|
5
|
+
* Env: AGENTCHAIN_BASE_URL, AGENTCHAIN_API_KEY (ag_…), AGENTCHAIN_LOCALE / AGENTCHAIN_LANG
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* One API error → stderr lines an agent can act on: message, code, nextStep, retryAfterSeconds
|
|
9
|
+
* (and the JSON body with --json). Returns the text so callers / tests can reuse it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatApiError(e: unknown, opts?: {
|
|
12
|
+
json?: boolean;
|
|
13
|
+
}): string;
|
|
14
|
+
export declare function usageText(): string;
|