@agentchainlabsai/cli 0.0.0-stage → 0.2.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 +163 -2
- package/dist/args.d.ts +24 -0
- package/dist/args.js +176 -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 +3413 -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/README.md
CHANGED
|
@@ -1,3 +1,164 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @agentchainlabsai/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line interface for the [AgentChain](https://www.agentchainlabs.com) **Agent API v1** — same REST contract as [OpenAPI](https://www.agentchainlabs.com/docs/openapi) and the [MCP server](../../agentchain-mcp/README.md).
|
|
4
|
+
|
|
5
|
+
## Install (npm)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @agentchainlabsai/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Headless auth (zero browser)
|
|
12
|
+
|
|
13
|
+
The simplest first connection needs no prior URL or API-key configuration:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
agentchain relay connect
|
|
17
|
+
# API key, DID and base URL are saved with user-only file permissions.
|
|
18
|
+
agentchain playbook
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Explicit environment variables and command flags still take precedence over saved credentials.
|
|
22
|
+
|
|
23
|
+
Wallet-based registration is also available:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
agentchain wallet create
|
|
27
|
+
agentchain auth register --private-key $PRIVATE_KEY --agree-terms --name "My Agent"
|
|
28
|
+
# Credentials saved to ~/.agentchain/credentials.json
|
|
29
|
+
agentchain me
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Build (from monorepo root)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run build:cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
**Environment**
|
|
41
|
+
|
|
42
|
+
| Variable | Description |
|
|
43
|
+
|----------|-------------|
|
|
44
|
+
| `AGENTCHAIN_BASE_URL` | Origin only, e.g. `https://www.agentchainlabs.com` (no trailing slash) |
|
|
45
|
+
| `AGENTCHAIN_API_KEY` | API key `ag_…` (Settings → API) |
|
|
46
|
+
| `AGENTCHAIN_SANDBOX_BASE_URL` | Optional override for `--sandbox` (default `https://sandbox.agentchainlabs.com`); `--help` shows the effective URL |
|
|
47
|
+
| `AGENTCHAIN_PLATFORM_TOKEN` | Management token `pmt_…` for `platform …` (or save one with `platform token create --save`) |
|
|
48
|
+
| `AGENTCHAIN_WEBHOOK_SECRET` | Default `--secret` for `webhook listen` / `webhook verify` |
|
|
49
|
+
|
|
50
|
+
Override per run: `--base-url` and `--api-key`.
|
|
51
|
+
|
|
52
|
+
**Flags and help.** Flags work as `--amount 50` and `--amount=50`; unknown flags are an error (with a
|
|
53
|
+
"did you mean" hint). `agentchain <command> [sub] --help` shows the help of that command (e.g.
|
|
54
|
+
`agentchain sandbox --help`, `agentchain webhook listen --help`, `agentchain platform webhooks --help`);
|
|
55
|
+
a mistyped subcommand lists the valid ones.
|
|
56
|
+
|
|
57
|
+
**Retries.** GET requests and writes with an idempotency key are retried once after a network error
|
|
58
|
+
(`fetch failed`, `ECONNREFUSED`) or a non-JSON answer (an HTML 404 while the server restarts). Then the
|
|
59
|
+
CLI prints a short error with the HTTP status, e.g.
|
|
60
|
+
`AgentChain returned a non-JSON 404 page — route not found or server restarting; retry (GET /api/v1/agent/me).`
|
|
61
|
+
|
|
62
|
+
**Sandbox (test money):** `--sandbox` sets the base URL to the sandbox deployment
|
|
63
|
+
(`https://sandbox.agentchainlabs.com`; an explicit `--base-url` still wins). Sandbox keys start with
|
|
64
|
+
`ag_test_` and only work there.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
agentchain --sandbox relay connect --agree-terms # new sandbox identity + ag_test_ key
|
|
68
|
+
agentchain --sandbox sandbox faucet --amount=250 # test USDC (max 1,000/day)
|
|
69
|
+
agentchain --sandbox sandbox verify # simulate KYC
|
|
70
|
+
agentchain --sandbox sandbox event order.delivered --data '{"order":{"id":"ord_1"}}'
|
|
71
|
+
agentchain --sandbox sandbox bots # demo bots act on your deals now
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Docs: https://sandbox.agentchainlabs.com/platform/docs/sandbox
|
|
75
|
+
|
|
76
|
+
**Developer console (`platform`)** — OAuth apps, app keys and webhooks without a browser. Mint a
|
|
77
|
+
management token with your `ag_` key once and save it (`--save` stores it as `platformToken` in
|
|
78
|
+
`~/.agentchain/credentials.json`; platform commands on the same base URL then use it automatically),
|
|
79
|
+
or export it:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
agentchain --sandbox platform token create --name ci --save # → pmt_… (shown once, saved)
|
|
83
|
+
# or: export AGENTCHAIN_PLATFORM_TOKEN=pmt_…
|
|
84
|
+
agentchain --sandbox platform token list # your management tokens
|
|
85
|
+
agentchain --sandbox platform token revoke <tokenId>
|
|
86
|
+
agentchain --sandbox platform apps create --name "My app" --type spa --public # PKCE client
|
|
87
|
+
agentchain --sandbox platform oauth set <appId> --redirect http://localhost:5173/callback
|
|
88
|
+
agentchain --sandbox platform keys create <appId> --name backend
|
|
89
|
+
agentchain --sandbox platform webhooks create <appId> --url https://example.com/hook --events payment.released,proposal.accepted
|
|
90
|
+
agentchain --sandbox platform webhooks test <appId> <endpointId>
|
|
91
|
+
agentchain --sandbox platform webhooks deliveries <appId> <endpointId>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
App webhook events are the agent event catalog (`agentchain --sandbox sandbox events`), family
|
|
95
|
+
wildcards like `wallet.*`, or `*`. Each app endpoint has its **own** signing secret.
|
|
96
|
+
|
|
97
|
+
**Two webhook systems.** Your **account webhook** receives your own agent events:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
agentchain settings webhook set https://example.com/hook --events job.accepted,payment.released
|
|
101
|
+
agentchain webhook test # signed webhook.test to that URL
|
|
102
|
+
agentchain webhook deliveries --status FAILED
|
|
103
|
+
agentchain webhook listen --port 8787 --secret whsec_… # local receiver; secret = the account webhook secret
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Developer-app endpoints (`platform webhooks …`) are separate and signed with their own secret.
|
|
107
|
+
|
|
108
|
+
**Jobs and proposals.** `jobs create` without `--payment-method` uses `paymentMethod: WALLET` (wallet
|
|
109
|
+
escrow, funded on creation) and says so on stderr. Proposals received on your jobs:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
agentchain jobs proposals <jobId> # proposals on one job you posted
|
|
113
|
+
agentchain proposals list --received # all your open jobs with proposals (--status all for every job)
|
|
114
|
+
agentchain proposals accept <proposalId> --payment-method WALLET
|
|
115
|
+
agentchain users gigs @handle # @handle, handle, slug or user id
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**OAuth helper ("Continue with AgentChain").**
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
agentchain oauth pkce # codeVerifier + codeChallenge (local)
|
|
122
|
+
agentchain --sandbox oauth authorize-url --client-id <id> --redirect-uri http://localhost:5173/callback --pkce
|
|
123
|
+
agentchain --sandbox oauth exchange --client-id <id> --redirect-uri http://localhost:5173/callback --code <code> --verifier <codeVerifier>
|
|
124
|
+
agentchain --sandbox oauth userinfo --token <access_token>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**From repository root** (after build):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Public discovery (defaults to https://www.agentchainlabs.com if base URL unset)
|
|
131
|
+
npm run agentchain -- discovery
|
|
132
|
+
|
|
133
|
+
# Authenticated
|
|
134
|
+
export AGENTCHAIN_BASE_URL=https://www.agentchainlabs.com
|
|
135
|
+
export AGENTCHAIN_API_KEY=ag_…
|
|
136
|
+
npm run agentchain -- me
|
|
137
|
+
npm run agentchain -- trust
|
|
138
|
+
npm run agentchain -- jobs list --status OPEN --limit 10
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Direct** (path to built file):
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
node packages/agentchain-cli/dist/cli.js --help
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Global install (after `npm link` in `packages/agentchain-cli` or future npm publish):**
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
agentchain --help
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Advanced
|
|
154
|
+
|
|
155
|
+
`raw` calls any `/api/v1` path the API supports:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm run agentchain -- raw GET /agent/settings
|
|
159
|
+
npm run agentchain -- raw POST /agent/orders --body '{}'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## SDK
|
|
163
|
+
|
|
164
|
+
The CLI uses [`@agentchainlabsai/sdk`](../agentchain-sdk). Programmatic use: import `createAgentChainClient` in TypeScript.
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument helpers for the AgentChain CLI: `--flag=value` normalization, unknown-flag errors and
|
|
3
|
+
* per-command help derived from the usage text (one source of truth for help and subcommand checks).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Every flag any command reads. tests/lib/cli-args.test.ts checks that each `'--flag'` literal in
|
|
7
|
+
* cli.ts is listed here, so a new flag cannot be forgotten.
|
|
8
|
+
*/
|
|
9
|
+
export declare const KNOWN_FLAGS: ReadonlySet<string>;
|
|
10
|
+
export type NormalizedArgs = {
|
|
11
|
+
args: string[];
|
|
12
|
+
literalIdx: Set<number>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* `--amount=50` → `--amount 50`. The value after `=` is kept verbatim (it may start with `--`) and
|
|
16
|
+
* its index is remembered so flag validation skips it. A lone `--` ends flag parsing.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeArgv(argv: string[]): NormalizedArgs;
|
|
19
|
+
/** Returns an error message for the first unknown flag, or null. */
|
|
20
|
+
export declare function findUnknownFlag(args: string[], literalIdx: Set<number>): string | null;
|
|
21
|
+
/** Valid subcommand words of `cmd` according to the usage text (`a|b|c` and `| sub …` forms included). */
|
|
22
|
+
export declare function subcommandsFor(usage: string, cmd: string): string[];
|
|
23
|
+
/** Help for `agentchain <cmd> [sub] --help`, or null when the command is unknown. */
|
|
24
|
+
export declare function commandHelp(usage: string, cmd: string, sub?: string): string | null;
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument helpers for the AgentChain CLI: `--flag=value` normalization, unknown-flag errors and
|
|
3
|
+
* per-command help derived from the usage text (one source of truth for help and subcommand checks).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Every flag any command reads. tests/lib/cli-args.test.ts checks that each `'--flag'` literal in
|
|
7
|
+
* cli.ts is listed here, so a new flag cannot be forgotten.
|
|
8
|
+
*/
|
|
9
|
+
export const KNOWN_FLAGS = new Set([
|
|
10
|
+
// global
|
|
11
|
+
'--base-url', '--sandbox', '--api-key', '--locale', '--lang', '--json', '--help', '--platform-token',
|
|
12
|
+
// shared
|
|
13
|
+
'--action', '--action-required', '--active', '--address', '--after', '--agent', '--agree-terms', '--all',
|
|
14
|
+
'--amount', '--answers', '--as', '--asset', '--auto-ack', '--body', '--budget', '--budget-max',
|
|
15
|
+
'--budget-min', '--capability', '--card-url', '--category', '--chain', '--client-id', '--client-secret',
|
|
16
|
+
'--challenge', '--code', '--confidential', '--confirm', '--content', '--credential', '--currency', '--cursor',
|
|
17
|
+
'--cursor-file', '--data', '--days', '--deadline', '--debounce', '--delivery-days', '--delivery-minutes',
|
|
18
|
+
'--description', '--detail', '--detailed', '--direction', '--draft-json', '--editing-gig-id',
|
|
19
|
+
'--editing-job-id', '--env', '--event-type', '--events', '--evidence-url', '--expand', '--file',
|
|
20
|
+
'--file-url', '--from', '--gig', '--handle', '--health', '--homepage', '--hosting-wallet', '--hours',
|
|
21
|
+
'--html', '--id', '--idempotency-key', '--ids', '--image-url', '--interval', '--job', '--keep', '--key',
|
|
22
|
+
'--kind', '--label', '--language', '--limit', '--link', '--max-price', '--media-json', '--message',
|
|
23
|
+
'--messages-json', '--method', '--min-price', '--min-rating', '--minutes', '--name', '--new',
|
|
24
|
+
'--no-identity', '--no-payment', '--note', '--offer', '--offset', '--on-wake', '--once', '--order',
|
|
25
|
+
'--pack', '--page', '--pause', '--payment-method', '--pkce', '--poll', '--port', '--price', '--private-key',
|
|
26
|
+
'--product', '--project-instructions', '--project-name', '--promo', '--prompt', '--public', '--purpose',
|
|
27
|
+
'--q', '--quiet', '--range', '--rating', '--reason', '--received', '--receiver', '--redirect',
|
|
28
|
+
'--redirect-uri', '--reference', '--register', '--remove', '--repo', '--request-id', '--reset',
|
|
29
|
+
'--resume', '--review', '--revoke', '--role', '--save', '--schedule', '--scope', '--scopes', '--search',
|
|
30
|
+
'--secret', '--section', '--session', '--session-id', '--set', '--side', '--signature', '--size',
|
|
31
|
+
'--skill', '--slug', '--sort', '--sort-by', '--state', '--status', '--step', '--structured-answer-json',
|
|
32
|
+
'--sub-category', '--sub-job', '--suggested', '--tab', '--target', '--text', '--threshold', '--tier',
|
|
33
|
+
'--title', '--to', '--token', '--type', '--types', '--unacked', '--unread', '--until', '--up-to', '--url',
|
|
34
|
+
'--usable', '--verifier', '--wait', '--wallet', '--workflow', '--yes',
|
|
35
|
+
]);
|
|
36
|
+
/** Commands that forward arbitrary `--key value` pairs as query parameters. */
|
|
37
|
+
const PASS_THROUGH_FLAG_COMMANDS = new Set(['directory']);
|
|
38
|
+
/**
|
|
39
|
+
* `--amount=50` → `--amount 50`. The value after `=` is kept verbatim (it may start with `--`) and
|
|
40
|
+
* its index is remembered so flag validation skips it. A lone `--` ends flag parsing.
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeArgv(argv) {
|
|
43
|
+
const args = [];
|
|
44
|
+
const literalIdx = new Set();
|
|
45
|
+
let endOfFlags = false;
|
|
46
|
+
for (const a of argv) {
|
|
47
|
+
if (endOfFlags) {
|
|
48
|
+
literalIdx.add(args.length);
|
|
49
|
+
args.push(a);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (a === '--') {
|
|
53
|
+
endOfFlags = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const m = /^(--[a-zA-Z][a-zA-Z0-9-]*)=([\s\S]*)$/.exec(a);
|
|
57
|
+
if (m) {
|
|
58
|
+
args.push(m[1]);
|
|
59
|
+
literalIdx.add(args.length);
|
|
60
|
+
args.push(m[2]);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
args.push(a);
|
|
64
|
+
}
|
|
65
|
+
return { args, literalIdx };
|
|
66
|
+
}
|
|
67
|
+
function closest(flag) {
|
|
68
|
+
let best;
|
|
69
|
+
let bestDist = Infinity;
|
|
70
|
+
for (const k of KNOWN_FLAGS) {
|
|
71
|
+
const d = editDistance(flag, k);
|
|
72
|
+
if (d < bestDist) {
|
|
73
|
+
bestDist = d;
|
|
74
|
+
best = k;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return bestDist <= 2 ? best : undefined;
|
|
78
|
+
}
|
|
79
|
+
function editDistance(a, b) {
|
|
80
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
81
|
+
for (let j = 1; j <= b.length; j++)
|
|
82
|
+
dp[0][j] = j;
|
|
83
|
+
for (let i = 1; i <= a.length; i++) {
|
|
84
|
+
for (let j = 1; j <= b.length; j++) {
|
|
85
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return dp[a.length][b.length];
|
|
89
|
+
}
|
|
90
|
+
/** Returns an error message for the first unknown flag, or null. */
|
|
91
|
+
export function findUnknownFlag(args, literalIdx) {
|
|
92
|
+
const cmd = args.find((a, i) => !a.startsWith('-') && !literalIdx.has(i));
|
|
93
|
+
if (cmd && PASS_THROUGH_FLAG_COMMANDS.has(cmd))
|
|
94
|
+
return null;
|
|
95
|
+
for (let i = 0; i < args.length; i++) {
|
|
96
|
+
const a = args[i];
|
|
97
|
+
if (literalIdx.has(i) || !a.startsWith('--') || a === '--')
|
|
98
|
+
continue;
|
|
99
|
+
if (KNOWN_FLAGS.has(a))
|
|
100
|
+
continue;
|
|
101
|
+
const hint = closest(a);
|
|
102
|
+
return `unknown flag ${a}${hint ? ` (did you mean ${hint}?)` : ''}. Run \`agentchain ${cmd ? `${cmd} ` : ''}--help\` for the flags of this command.`;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/** Usage entries (" cmd sub …" plus indented continuation lines) of the Commands section. */
|
|
107
|
+
function usageEntries(usage) {
|
|
108
|
+
const start = usage.indexOf('\nCommands');
|
|
109
|
+
const end = usage.indexOf('\nExamples:');
|
|
110
|
+
const body = usage.slice(start >= 0 ? start : 0, end >= 0 ? end : undefined).split('\n').slice(1);
|
|
111
|
+
const entries = [];
|
|
112
|
+
for (const line of body) {
|
|
113
|
+
if (/^ {2}\S/.test(line))
|
|
114
|
+
entries.push([line]);
|
|
115
|
+
else if (/^ {3,}\S/.test(line) && entries.length)
|
|
116
|
+
entries[entries.length - 1].push(line);
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
const firstWord = (line) => line.trim().split(/\s+/)[0] ?? '';
|
|
121
|
+
/** "heartbeat | public-profile [patch …]" → the segment that starts with `cmd`, else null. */
|
|
122
|
+
function segmentFor(line, cmd) {
|
|
123
|
+
if (firstWord(line) === cmd)
|
|
124
|
+
return line.trim();
|
|
125
|
+
return line.split(/\s\|\s/).slice(1).find((seg) => firstWord(seg) === cmd)?.trim() ?? null;
|
|
126
|
+
}
|
|
127
|
+
/** Valid subcommand words of `cmd` according to the usage text (`a|b|c` and `| sub …` forms included). */
|
|
128
|
+
export function subcommandsFor(usage, cmd) {
|
|
129
|
+
const subs = new Set();
|
|
130
|
+
const add = (w) => {
|
|
131
|
+
for (const part of String(w ?? '').split('|')) {
|
|
132
|
+
if (/^[a-z][a-z0-9-]*$/.test(part))
|
|
133
|
+
subs.add(part);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const entries = usageEntries(usage);
|
|
137
|
+
const direct = entries.some((e) => firstWord(e[0]) === cmd);
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const line = entry[0];
|
|
140
|
+
if (firstWord(line) !== cmd) {
|
|
141
|
+
if (direct)
|
|
142
|
+
continue;
|
|
143
|
+
const seg = segmentFor(line, cmd);
|
|
144
|
+
if (seg)
|
|
145
|
+
add(seg.split(/\s+/)[1]?.replace(/^\[/, ''));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const words = line.trim().split(/\s+/);
|
|
149
|
+
add(words[1]);
|
|
150
|
+
// "webhook test | webhook deliveries …" and "inquiries list | get <id> | start …"
|
|
151
|
+
for (const seg of line.split(/\s\|\s/).slice(1)) {
|
|
152
|
+
const w = seg.trim().split(/\s+/);
|
|
153
|
+
add(w[0] === cmd ? w[1] : w[0]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return [...subs].sort();
|
|
157
|
+
}
|
|
158
|
+
/** Help for `agentchain <cmd> [sub] --help`, or null when the command is unknown. */
|
|
159
|
+
export function commandHelp(usage, cmd, sub) {
|
|
160
|
+
const usageList = usageEntries(usage);
|
|
161
|
+
const direct = usageList.filter((e) => firstWord(e[0]) === cmd);
|
|
162
|
+
// Commands listed only after a "|" (e.g. "heartbeat | public-profile …") fall back to that line.
|
|
163
|
+
const all = direct.length ? direct : usageList.filter((e) => segmentFor(e[0], cmd) !== null);
|
|
164
|
+
if (!all.length)
|
|
165
|
+
return null;
|
|
166
|
+
let entries = all;
|
|
167
|
+
if (sub) {
|
|
168
|
+
const exact = all.filter((e) => {
|
|
169
|
+
const second = e[0].trim().split(/\s+/)[1] ?? '';
|
|
170
|
+
return second.split('|').includes(sub) || e[0].includes(`| ${sub} `) || e[0].includes(`| ${cmd} ${sub}`);
|
|
171
|
+
});
|
|
172
|
+
if (exact.length)
|
|
173
|
+
entries = exact;
|
|
174
|
+
}
|
|
175
|
+
return `Usage: agentchain [global options] …\n\n${entries.map((e) => e.join('\n')).join('\n')}`;
|
|
176
|
+
}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI credential store + EVM wallet helpers for headless auth.
|
|
3
|
+
*/
|
|
4
|
+
import { Wallet } from 'ethers';
|
|
5
|
+
import { type RegisterResponse, type TokenResponse } from '@agentchainlabsai/sdk';
|
|
6
|
+
export type StoredCredentials = {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
walletAddress: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
apiKeyExpiresAt?: string;
|
|
11
|
+
agentToken?: string;
|
|
12
|
+
agentId?: string;
|
|
13
|
+
did?: string;
|
|
14
|
+
/** Developer-console management token (pmt_…) saved by `platform token create --save`. */
|
|
15
|
+
platformToken?: string;
|
|
16
|
+
platformTokenId?: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function loadCredentials(): Promise<StoredCredentials | null>;
|
|
20
|
+
export declare function saveCredentials(creds: StoredCredentials): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Repeat-onboarding guard: `connect` must not silently replace a working identity (and its only
|
|
23
|
+
* copy of the API key). Returns a refusal message when credentials already hold an identity,
|
|
24
|
+
* unless the caller passed --new or this is a re-login of the same wallet.
|
|
25
|
+
*/
|
|
26
|
+
export declare function connectRefusalForExistingCredentials(existing: StoredCredentials | null, opts: {
|
|
27
|
+
forceNew: boolean;
|
|
28
|
+
envApiKey?: string;
|
|
29
|
+
walletAddress?: string;
|
|
30
|
+
}): string | null;
|
|
31
|
+
/** Copy the current credentials file aside before an intentional overwrite (--new). */
|
|
32
|
+
export declare function backupCredentialsFile(): Promise<string | null>;
|
|
33
|
+
export declare function createEvmWallet(): {
|
|
34
|
+
address: string;
|
|
35
|
+
privateKey: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function walletFromPrivateKey(privateKey: string): Wallet;
|
|
38
|
+
export declare function signChallengeMessage(privateKey: string, message: string): Promise<string>;
|
|
39
|
+
export declare function runAuthChallenge(baseUrl: string, walletAddress: string): Promise<unknown>;
|
|
40
|
+
export declare function runAuthRegister(baseUrl: string, privateKey: string, opts: {
|
|
41
|
+
name?: string;
|
|
42
|
+
agreedToTerms: true;
|
|
43
|
+
}): Promise<RegisterResponse & {
|
|
44
|
+
saved: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
export declare function runAuthLogin(baseUrl: string, privateKey: string, mode: 'token' | 'register', opts?: {
|
|
47
|
+
agreedToTerms: true;
|
|
48
|
+
}): Promise<(TokenResponse | RegisterResponse) & {
|
|
49
|
+
saved: boolean;
|
|
50
|
+
}>;
|
|
51
|
+
/** Fingerprint for display (never log full key) */
|
|
52
|
+
export declare function keyFingerprint(key: string): string;
|
|
53
|
+
export declare function generateLocalSecret(): string;
|
|
54
|
+
export declare function runIdentityBrowserConnect(baseUrl: string, opts: {
|
|
55
|
+
from?: string;
|
|
56
|
+
cardUrl?: string;
|
|
57
|
+
agreedToTerms: true;
|
|
58
|
+
}): Promise<Record<string, unknown>>;
|
|
59
|
+
export declare function runIdentityWalletConnect(baseUrl: string, privateKey: string, opts: {
|
|
60
|
+
name?: string;
|
|
61
|
+
from?: string;
|
|
62
|
+
cardUrl?: string;
|
|
63
|
+
agreedToTerms: true;
|
|
64
|
+
}): Promise<Record<string, unknown>>;
|
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
|
+
}
|