aport-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/dist/cli.js +92 -60
- package/dist/identity.js +89 -23
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,24 +2,23 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* aport — A-port command-line client for AI agents.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Multiple identities on one machine:
|
|
6
|
+
* aport keygen creator # create a named identity
|
|
7
|
+
* aport keygen fan
|
|
8
|
+
* aport accounts # list identities, show active
|
|
9
|
+
* aport use creator # switch active account
|
|
10
|
+
* export APORT_ACCOUNT=fan # bind an account to a shell/Hermes session
|
|
11
|
+
* aport --account fan publish ... # per-command override
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* npx aport-cli publish --ns "<addr>.topic.test" --desc "..." --price 5 --file ./c.txt
|
|
11
|
-
* npx aport-cli buy --id <uuid>
|
|
12
|
-
* npx aport-cli subscribe --ns "crypto_sentinel.event.flashcrash"
|
|
13
|
-
*
|
|
14
|
-
* Writes (publish/buy) are signed with your key in ~/.aport/key.
|
|
15
|
-
* Target API base URL: --url, or APORT_API_URL, or the hosted default.
|
|
13
|
+
* Then: search / publish / buy / subscribe over the signed HTTP API.
|
|
14
|
+
* Target API: --url, or APORT_API_URL, or the hosted default.
|
|
16
15
|
*/
|
|
17
16
|
import { readFile } from "node:fs/promises";
|
|
18
17
|
import { Command } from "commander";
|
|
19
|
-
import { generate,
|
|
18
|
+
import { accountExists, addressForName, generate, getActiveName, keyPath, listAccountNames, load, setActive, signRequest, } from "./identity.js";
|
|
20
19
|
const DEFAULT_API_URL = "https://a-port.vercel.app";
|
|
21
20
|
/* --------------------------------------------------------------------------- */
|
|
22
|
-
/* tiny ANSI helpers
|
|
21
|
+
/* tiny ANSI helpers */
|
|
23
22
|
/* --------------------------------------------------------------------------- */
|
|
24
23
|
const useColor = process.stdout.isTTY;
|
|
25
24
|
const paint = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
@@ -35,6 +34,17 @@ function baseUrl(opts) {
|
|
|
35
34
|
const url = opts.url ?? process.env.APORT_API_URL ?? DEFAULT_API_URL;
|
|
36
35
|
return url.replace(/\/+$/, "");
|
|
37
36
|
}
|
|
37
|
+
/** Load the identity for this command, or print an error + exit. */
|
|
38
|
+
function loadOrExit(g) {
|
|
39
|
+
try {
|
|
40
|
+
return load(g.account);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error(red(err instanceof Error ? err.message : String(err)));
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
38
48
|
async function fetchJson(url, init) {
|
|
39
49
|
let res;
|
|
40
50
|
try {
|
|
@@ -60,16 +70,8 @@ function errorMessage(json, fallback) {
|
|
|
60
70
|
}
|
|
61
71
|
return fallback;
|
|
62
72
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return true;
|
|
66
|
-
console.error(red("no identity found — run `aport keygen` first"));
|
|
67
|
-
process.exitCode = 1;
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
/** POST a signed JSON request. Returns the parsed response. */
|
|
71
|
-
async function signedPost(g, path, bodyObject) {
|
|
72
|
-
const id = await load();
|
|
73
|
+
/** POST a signed JSON request as the given identity. */
|
|
74
|
+
async function signedPost(g, id, path, bodyObject) {
|
|
73
75
|
const body = JSON.stringify(bodyObject);
|
|
74
76
|
const headers = {
|
|
75
77
|
"Content-Type": "application/json",
|
|
@@ -92,14 +94,14 @@ function renderTable(rows) {
|
|
|
92
94
|
console.log(row(cols.map((c) => c.get(r))));
|
|
93
95
|
}
|
|
94
96
|
/* --------------------------------------------------------------------------- */
|
|
95
|
-
/* SSE subscription
|
|
97
|
+
/* SSE subscription */
|
|
96
98
|
/* --------------------------------------------------------------------------- */
|
|
97
99
|
function handleSseFrame(frame, ns) {
|
|
98
100
|
let event = "message";
|
|
99
101
|
const dataLines = [];
|
|
100
102
|
for (const line of frame.split("\n")) {
|
|
101
103
|
if (line.startsWith(":"))
|
|
102
|
-
continue;
|
|
104
|
+
continue;
|
|
103
105
|
if (line.startsWith("event:"))
|
|
104
106
|
event = line.slice(6).trim();
|
|
105
107
|
else if (line.startsWith("data:"))
|
|
@@ -118,7 +120,7 @@ function handleSseFrame(frame, ns) {
|
|
|
118
120
|
rendered = JSON.stringify(JSON.parse(data), null, 2);
|
|
119
121
|
}
|
|
120
122
|
catch {
|
|
121
|
-
/* not JSON
|
|
123
|
+
/* not JSON */
|
|
122
124
|
}
|
|
123
125
|
console.log(green(`[${ts}] ▶ ${event.toUpperCase()} @ ${ns}`));
|
|
124
126
|
console.log(rendered);
|
|
@@ -146,10 +148,9 @@ async function streamSse(url, ns) {
|
|
|
146
148
|
buffer += decoder.decode(value, { stream: true });
|
|
147
149
|
const frames = buffer.split("\n\n");
|
|
148
150
|
buffer = frames.pop() ?? "";
|
|
149
|
-
for (const frame of frames)
|
|
151
|
+
for (const frame of frames)
|
|
150
152
|
if (frame.trim())
|
|
151
153
|
handleSseFrame(frame, ns);
|
|
152
|
-
}
|
|
153
154
|
}
|
|
154
155
|
console.log(dim("-- stream ended --"));
|
|
155
156
|
}
|
|
@@ -159,36 +160,69 @@ async function streamSse(url, ns) {
|
|
|
159
160
|
const program = new Command();
|
|
160
161
|
program
|
|
161
162
|
.name("aport")
|
|
162
|
-
.description("A-port CLI — identity, publish, search, buy,
|
|
163
|
-
.version("0.
|
|
164
|
-
.option("-u, --url <url>", "API base URL (default APORT_API_URL or the hosted A-port)")
|
|
163
|
+
.description("A-port CLI — multi-account identity, publish, search, buy, subscribe.")
|
|
164
|
+
.version("0.3.0")
|
|
165
|
+
.option("-u, --url <url>", "API base URL (default APORT_API_URL or the hosted A-port)")
|
|
166
|
+
.option("--account <name>", "use this account (overrides $APORT_ACCOUNT / active)");
|
|
167
|
+
/* ---- identity / accounts ---- */
|
|
165
168
|
program
|
|
166
169
|
.command("keygen")
|
|
167
|
-
.description("Create a local agent identity (keypair) and print
|
|
168
|
-
.
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.error(red("
|
|
173
|
-
console.error(dim(` ${keyPath()} —
|
|
170
|
+
.description("Create a local agent identity (keypair) and print its address.")
|
|
171
|
+
.argument("[name]", "account name", "default")
|
|
172
|
+
.option("--force", "overwrite an existing account (you lose its address)")
|
|
173
|
+
.action((name, opts) => {
|
|
174
|
+
if (accountExists(name) && !opts.force) {
|
|
175
|
+
console.error(red(`account "${name}" already exists: `) + cyan(addressForName(name)));
|
|
176
|
+
console.error(dim(` ${keyPath(name)} — use --force to overwrite`));
|
|
174
177
|
process.exitCode = 1;
|
|
175
178
|
return;
|
|
176
179
|
}
|
|
177
|
-
const id =
|
|
178
|
-
console.log(green(
|
|
180
|
+
const id = generate(name);
|
|
181
|
+
console.log(green(`✓ identity "${id.name}" created`));
|
|
179
182
|
console.log(` address: ${cyan(id.address)}`);
|
|
180
|
-
console.log(` saved: ${keyPath()} (chmod 600)`);
|
|
181
|
-
|
|
183
|
+
console.log(` saved: ${keyPath(name)} (chmod 600 — back this up!)`);
|
|
184
|
+
if (getActiveName() === name)
|
|
185
|
+
console.log(dim(" (now the active account)"));
|
|
182
186
|
});
|
|
183
187
|
program
|
|
184
|
-
.command("
|
|
185
|
-
.description("
|
|
186
|
-
.action(
|
|
187
|
-
|
|
188
|
+
.command("accounts")
|
|
189
|
+
.description("List local identities and show the active one.")
|
|
190
|
+
.action(() => {
|
|
191
|
+
const names = listAccountNames();
|
|
192
|
+
if (names.length === 0) {
|
|
193
|
+
console.log(dim("no accounts — run `aport keygen`"));
|
|
188
194
|
return;
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
}
|
|
196
|
+
const active = getActiveName();
|
|
197
|
+
for (const n of names) {
|
|
198
|
+
const marker = n === active ? green(" * ") : " ";
|
|
199
|
+
console.log(`${marker}${n.padEnd(16)} ${dim(addressForName(n))}`);
|
|
200
|
+
}
|
|
191
201
|
});
|
|
202
|
+
program
|
|
203
|
+
.command("use")
|
|
204
|
+
.description("Switch the active account.")
|
|
205
|
+
.argument("<name>", "account name")
|
|
206
|
+
.action((name) => {
|
|
207
|
+
try {
|
|
208
|
+
setActive(name);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
console.error(red(err instanceof Error ? err.message : String(err)));
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
console.log(green(`✓ active account → ${name}`) + dim(` ${addressForName(name)}`));
|
|
216
|
+
});
|
|
217
|
+
program
|
|
218
|
+
.command("whoami")
|
|
219
|
+
.description("Print the active account's address.")
|
|
220
|
+
.action((_opts, command) => {
|
|
221
|
+
const id = loadOrExit(command.optsWithGlobals());
|
|
222
|
+
if (id)
|
|
223
|
+
console.log(id.address);
|
|
224
|
+
});
|
|
225
|
+
/* ---- marketplace ---- */
|
|
192
226
|
program
|
|
193
227
|
.command("publish")
|
|
194
228
|
.description("Publish an article from a file under your namespace (signed).")
|
|
@@ -197,9 +231,10 @@ program
|
|
|
197
231
|
.requiredOption("--file <path>", "path to the content file")
|
|
198
232
|
.option("--price <usd>", "price in USD", "0")
|
|
199
233
|
.action(async (opts, command) => {
|
|
200
|
-
if (!requireKey())
|
|
201
|
-
return;
|
|
202
234
|
const g = command.optsWithGlobals();
|
|
235
|
+
const id = loadOrExit(g);
|
|
236
|
+
if (!id)
|
|
237
|
+
return;
|
|
203
238
|
let body;
|
|
204
239
|
try {
|
|
205
240
|
body = await readFile(opts.file, "utf8");
|
|
@@ -209,7 +244,7 @@ program
|
|
|
209
244
|
process.exitCode = 1;
|
|
210
245
|
return;
|
|
211
246
|
}
|
|
212
|
-
const { res, json } = await signedPost(g, "/api/articles/publish", {
|
|
247
|
+
const { res, json } = await signedPost(g, id, "/api/articles/publish", {
|
|
213
248
|
namespace: opts.ns,
|
|
214
249
|
description: opts.desc,
|
|
215
250
|
body,
|
|
@@ -221,9 +256,8 @@ program
|
|
|
221
256
|
return;
|
|
222
257
|
}
|
|
223
258
|
const data = json;
|
|
224
|
-
console.log(green(
|
|
259
|
+
console.log(green(`✓ published as ${id.name}`));
|
|
225
260
|
console.log(` namespace : ${cyan(data.namespace)}`);
|
|
226
|
-
console.log(` author : ${data.author}`);
|
|
227
261
|
console.log(` article_id: ${data.id}`);
|
|
228
262
|
console.log(` price : $${Number(opts.price).toFixed(2)} (${body.length} bytes)`);
|
|
229
263
|
});
|
|
@@ -253,10 +287,11 @@ program
|
|
|
253
287
|
.description("Buy an article and print the decrypted content (signed).")
|
|
254
288
|
.requiredOption("--id <uuid>", "article id to buy")
|
|
255
289
|
.action(async (opts, command) => {
|
|
256
|
-
if (!requireKey())
|
|
257
|
-
return;
|
|
258
290
|
const g = command.optsWithGlobals();
|
|
259
|
-
const
|
|
291
|
+
const id = loadOrExit(g);
|
|
292
|
+
if (!id)
|
|
293
|
+
return;
|
|
294
|
+
const { res, json } = await signedPost(g, id, "/api/payment/checkout", {
|
|
260
295
|
articleId: opts.id,
|
|
261
296
|
});
|
|
262
297
|
if (!res.ok) {
|
|
@@ -265,12 +300,10 @@ program
|
|
|
265
300
|
return;
|
|
266
301
|
}
|
|
267
302
|
const data = json;
|
|
268
|
-
const me = await load();
|
|
269
303
|
console.log(green(data.alreadyOwned ? "✓ already owned — access granted" : "✓ payment confirmed"));
|
|
270
|
-
console.log(` buyer : ${
|
|
304
|
+
console.log(` buyer : ${id.name} (${id.address})`);
|
|
271
305
|
console.log(` namespace : ${cyan(data.namespace ?? "(none)")}`);
|
|
272
306
|
console.log(` paid : $${Number(data.pricePaidUsd).toFixed(2)}`);
|
|
273
|
-
console.log(` purchase : ${data.purchaseId}`);
|
|
274
307
|
console.log(dim("\n──────── DECRYPTED CONTENT ────────"));
|
|
275
308
|
console.log(data.content);
|
|
276
309
|
console.log(dim("───────────────────────────────────"));
|
|
@@ -281,8 +314,7 @@ program
|
|
|
281
314
|
.requiredOption("--ns <namespace>", "namespace to listen on")
|
|
282
315
|
.action(async (opts, command) => {
|
|
283
316
|
const g = command.optsWithGlobals();
|
|
284
|
-
|
|
285
|
-
await streamSse(url, opts.ns);
|
|
317
|
+
await streamSse(`${baseUrl(g)}/api/events/listen?ns=${encodeURIComponent(opts.ns)}`, opts.ns);
|
|
286
318
|
});
|
|
287
319
|
program.parseAsync(process.argv).catch((err) => {
|
|
288
320
|
console.error(red(err instanceof Error ? err.message : String(err)));
|
package/dist/identity.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent identity —
|
|
2
|
+
* Agent identity — multiple named ed25519 keypairs, switchable.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* ~/.aport/accounts/<name>.key one PEM key per account
|
|
5
|
+
* ~/.aport/active name of the active account
|
|
6
|
+
* ~/.aport/key legacy single key (auto-adopted as "default")
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* Which account a command uses: --account <name> > $APORT_ACCOUNT > active.
|
|
9
|
+
* address = "aport1" + base58( sha256(pubkey)[0..20] ).
|
|
8
10
|
*/
|
|
9
11
|
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, } from "node:crypto";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
11
|
-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
|
|
12
13
|
import { homedir } from "node:os";
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
const DIR = join(homedir(), ".aport");
|
|
15
|
-
const
|
|
16
|
+
const ACCOUNTS_DIR = join(DIR, "accounts");
|
|
17
|
+
const ACTIVE_FILE = join(DIR, "active");
|
|
18
|
+
const LEGACY_KEY = join(DIR, "key");
|
|
16
19
|
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
17
20
|
export function base58(buf) {
|
|
18
21
|
let x = BigInt("0x" + (buf.toString("hex") || "0"));
|
|
@@ -37,31 +40,94 @@ export function addressFromRaw(rawPub) {
|
|
|
37
40
|
const h = createHash("sha256").update(rawPub).digest();
|
|
38
41
|
return "aport1" + base58(h.subarray(0, 20));
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
return
|
|
43
|
+
function keyPathForName(name) {
|
|
44
|
+
return join(ACCOUNTS_DIR, `${name}.key`);
|
|
42
45
|
}
|
|
43
|
-
export function
|
|
44
|
-
return
|
|
46
|
+
export function keyPath(name) {
|
|
47
|
+
return keyPathForName(name);
|
|
45
48
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
/** One-time: adopt a legacy ~/.aport/key as account "default". */
|
|
50
|
+
function bootstrap() {
|
|
51
|
+
if (existsSync(LEGACY_KEY) && !existsSync(keyPathForName("default"))) {
|
|
52
|
+
mkdirSync(ACCOUNTS_DIR, { recursive: true });
|
|
53
|
+
copyFileSync(LEGACY_KEY, keyPathForName("default"));
|
|
54
|
+
chmodSync(keyPathForName("default"), 0o600);
|
|
55
|
+
if (!existsSync(ACTIVE_FILE))
|
|
56
|
+
writeFileSync(ACTIVE_FILE, "default\n");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function listAccountNames() {
|
|
60
|
+
bootstrap();
|
|
61
|
+
if (!existsSync(ACCOUNTS_DIR))
|
|
62
|
+
return [];
|
|
63
|
+
return readdirSync(ACCOUNTS_DIR)
|
|
64
|
+
.filter((f) => f.endsWith(".key"))
|
|
65
|
+
.map((f) => f.slice(0, -4))
|
|
66
|
+
.sort();
|
|
67
|
+
}
|
|
68
|
+
export function accountExists(name) {
|
|
69
|
+
return existsSync(keyPathForName(name));
|
|
70
|
+
}
|
|
71
|
+
export function getActiveName() {
|
|
72
|
+
if (existsSync(ACTIVE_FILE)) {
|
|
73
|
+
const n = readFileSync(ACTIVE_FILE, "utf8").trim();
|
|
74
|
+
if (n)
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
export function setActive(name) {
|
|
80
|
+
if (!accountExists(name))
|
|
81
|
+
throw new Error(`account "${name}" not found`);
|
|
82
|
+
mkdirSync(DIR, { recursive: true });
|
|
83
|
+
writeFileSync(ACTIVE_FILE, name + "\n");
|
|
54
84
|
}
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
/** Resolve which account name to use. */
|
|
86
|
+
export function resolveAccountName(explicit) {
|
|
87
|
+
bootstrap();
|
|
88
|
+
return (explicit ??
|
|
89
|
+
(process.env.APORT_ACCOUNT || undefined) ??
|
|
90
|
+
getActiveName() ??
|
|
91
|
+
(accountExists("default") ? "default" : null));
|
|
92
|
+
}
|
|
93
|
+
function loadFromPath(path, name) {
|
|
94
|
+
const pem = readFileSync(path, "utf8");
|
|
57
95
|
const privateKey = createPrivateKey(pem);
|
|
58
96
|
const raw = rawPublicKey(createPublicKey(privateKey));
|
|
59
|
-
return { privateKey, publicKeyRaw: raw, address: addressFromRaw(raw) };
|
|
97
|
+
return { name, privateKey, publicKeyRaw: raw, address: addressFromRaw(raw) };
|
|
98
|
+
}
|
|
99
|
+
export function addressForName(name) {
|
|
100
|
+
return loadFromPath(keyPathForName(name), name).address;
|
|
101
|
+
}
|
|
102
|
+
/** Load the identity for a command (respects --account / env / active). */
|
|
103
|
+
export function load(explicit) {
|
|
104
|
+
const name = resolveAccountName(explicit);
|
|
105
|
+
if (!name) {
|
|
106
|
+
throw new Error("no identity — run `aport keygen` (or `aport keygen --account <name>`)");
|
|
107
|
+
}
|
|
108
|
+
if (!accountExists(name)) {
|
|
109
|
+
throw new Error(`account "${name}" not found — run \`aport keygen --account ${name}\``);
|
|
110
|
+
}
|
|
111
|
+
return loadFromPath(keyPathForName(name), name);
|
|
112
|
+
}
|
|
113
|
+
/** Create a new named identity. Returns it. */
|
|
114
|
+
export function generate(name) {
|
|
115
|
+
bootstrap();
|
|
116
|
+
mkdirSync(ACCOUNTS_DIR, { recursive: true });
|
|
117
|
+
const { privateKey } = generateKeyPairSync("ed25519");
|
|
118
|
+
const path = keyPathForName(name);
|
|
119
|
+
writeFileSync(path, privateKey.export({ type: "pkcs8", format: "pem" }), {
|
|
120
|
+
mode: 0o600,
|
|
121
|
+
});
|
|
122
|
+
chmodSync(path, 0o600);
|
|
123
|
+
if (!getActiveName())
|
|
124
|
+
setActive(name); // first account becomes active
|
|
125
|
+
return loadFromPath(path, name);
|
|
60
126
|
}
|
|
61
127
|
export function canonical(method, path, bodyHashHex, ts, nonce) {
|
|
62
128
|
return ["APORT-AUTH-v1", method.toUpperCase(), path, bodyHashHex, ts, nonce].join("\n");
|
|
63
129
|
}
|
|
64
|
-
/** Build the signed auth headers for a request
|
|
130
|
+
/** Build the signed auth headers for a request. */
|
|
65
131
|
export function signRequest(id, method, path, body) {
|
|
66
132
|
const ts = Date.now().toString();
|
|
67
133
|
const nonce = randomBytes(16).toString("hex");
|