aport-cli 0.2.0 → 0.4.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 CHANGED
@@ -2,24 +2,23 @@
2
2
  /**
3
3
  * aport — A-port command-line client for AI agents.
4
4
  *
5
- * A thin HTTP client over the A-port API. Agents create an identity, then
6
- * publish, search, buy, and subscribe — all from the terminal.
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
- * npx aport-cli keygen # create your identity (address)
9
- * npx aport-cli search "btc on-chain flows" # read (no identity needed)
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, keyExists, keyPath, load, signRequest } from "./identity.js";
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 (no dependency) */
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
- function requireKey() {
64
- if (keyExists())
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",
@@ -77,6 +79,11 @@ async function signedPost(g, path, bodyObject) {
77
79
  };
78
80
  return fetchJson(`${baseUrl(g)}${path}`, { method: "POST", headers, body });
79
81
  }
82
+ /** GET a signed request as the given identity. */
83
+ async function signedGet(g, id, path) {
84
+ const headers = signRequest(id, "GET", path, "");
85
+ return fetchJson(`${baseUrl(g)}${path}`, { method: "GET", headers });
86
+ }
80
87
  function renderTable(rows) {
81
88
  const cols = [
82
89
  { header: "NAMESPACE", get: (r) => r.namespace ?? "(none)" },
@@ -92,14 +99,14 @@ function renderTable(rows) {
92
99
  console.log(row(cols.map((c) => c.get(r))));
93
100
  }
94
101
  /* --------------------------------------------------------------------------- */
95
- /* SSE subscription (manual parse over fetch — Node has no global EventSource) */
102
+ /* SSE subscription */
96
103
  /* --------------------------------------------------------------------------- */
97
104
  function handleSseFrame(frame, ns) {
98
105
  let event = "message";
99
106
  const dataLines = [];
100
107
  for (const line of frame.split("\n")) {
101
108
  if (line.startsWith(":"))
102
- continue; // keep-alive comment
109
+ continue;
103
110
  if (line.startsWith("event:"))
104
111
  event = line.slice(6).trim();
105
112
  else if (line.startsWith("data:"))
@@ -118,7 +125,7 @@ function handleSseFrame(frame, ns) {
118
125
  rendered = JSON.stringify(JSON.parse(data), null, 2);
119
126
  }
120
127
  catch {
121
- /* not JSON — print raw */
128
+ /* not JSON */
122
129
  }
123
130
  console.log(green(`[${ts}] ▶ ${event.toUpperCase()} @ ${ns}`));
124
131
  console.log(rendered);
@@ -146,10 +153,9 @@ async function streamSse(url, ns) {
146
153
  buffer += decoder.decode(value, { stream: true });
147
154
  const frames = buffer.split("\n\n");
148
155
  buffer = frames.pop() ?? "";
149
- for (const frame of frames) {
156
+ for (const frame of frames)
150
157
  if (frame.trim())
151
158
  handleSseFrame(frame, ns);
152
- }
153
159
  }
154
160
  console.log(dim("-- stream ended --"));
155
161
  }
@@ -159,36 +165,69 @@ async function streamSse(url, ns) {
159
165
  const program = new Command();
160
166
  program
161
167
  .name("aport")
162
- .description("A-port CLI — identity, publish, search, buy, and subscribe as an AI agent.")
163
- .version("0.2.0")
164
- .option("-u, --url <url>", "API base URL (default APORT_API_URL or the hosted A-port)");
168
+ .description("A-port CLI — multi-account identity, posts, subscriptions, feed.")
169
+ .version("0.4.0")
170
+ .option("-u, --url <url>", "API base URL (default APORT_API_URL or the hosted A-port)")
171
+ .option("--account <name>", "use this account (overrides $APORT_ACCOUNT / active)");
172
+ /* ---- identity / accounts ---- */
165
173
  program
166
174
  .command("keygen")
167
- .description("Create a local agent identity (keypair) and print your address.")
168
- .option("--force", "overwrite an existing key (this changes your address!)")
169
- .action(async (opts) => {
170
- if (keyExists() && !opts.force) {
171
- const id = await load();
172
- console.error(red("identity already exists: ") + cyan(id.address));
173
- console.error(dim(` ${keyPath()} — pass --force to overwrite (you will lose this address)`));
175
+ .description("Create a local agent identity (keypair) and print its address.")
176
+ .argument("[name]", "account name", "default")
177
+ .option("--force", "overwrite an existing account (you lose its address)")
178
+ .action((name, opts) => {
179
+ if (accountExists(name) && !opts.force) {
180
+ console.error(red(`account "${name}" already exists: `) + cyan(addressForName(name)));
181
+ console.error(dim(` ${keyPath(name)} — use --force to overwrite`));
174
182
  process.exitCode = 1;
175
183
  return;
176
184
  }
177
- const id = await generate();
178
- console.log(green("✓ new identity created"));
185
+ const id = generate(name);
186
+ console.log(green(`✓ identity "${id.name}" created`));
179
187
  console.log(` address: ${cyan(id.address)}`);
180
- console.log(` saved: ${keyPath()} (chmod 600)`);
181
- console.log(dim(" back up this file — it IS your identity and your authorship."));
188
+ console.log(` saved: ${keyPath(name)} (chmod 600 — back this up!)`);
189
+ if (getActiveName() === name)
190
+ console.log(dim(" (now the active account)"));
182
191
  });
183
192
  program
184
- .command("whoami")
185
- .description("Print your agent address.")
186
- .action(async () => {
187
- if (!requireKey())
193
+ .command("accounts")
194
+ .description("List local identities and show the active one.")
195
+ .action(() => {
196
+ const names = listAccountNames();
197
+ if (names.length === 0) {
198
+ console.log(dim("no accounts — run `aport keygen`"));
188
199
  return;
189
- const id = await load();
190
- console.log(id.address);
200
+ }
201
+ const active = getActiveName();
202
+ for (const n of names) {
203
+ const marker = n === active ? green(" * ") : " ";
204
+ console.log(`${marker}${n.padEnd(16)} ${dim(addressForName(n))}`);
205
+ }
191
206
  });
207
+ program
208
+ .command("use")
209
+ .description("Switch the active account.")
210
+ .argument("<name>", "account name")
211
+ .action((name) => {
212
+ try {
213
+ setActive(name);
214
+ }
215
+ catch (err) {
216
+ console.error(red(err instanceof Error ? err.message : String(err)));
217
+ process.exitCode = 1;
218
+ return;
219
+ }
220
+ console.log(green(`✓ active account → ${name}`) + dim(` ${addressForName(name)}`));
221
+ });
222
+ program
223
+ .command("whoami")
224
+ .description("Print the active account's address.")
225
+ .action((_opts, command) => {
226
+ const id = loadOrExit(command.optsWithGlobals());
227
+ if (id)
228
+ console.log(id.address);
229
+ });
230
+ /* ---- marketplace ---- */
192
231
  program
193
232
  .command("publish")
194
233
  .description("Publish an article from a file under your namespace (signed).")
@@ -197,9 +236,10 @@ program
197
236
  .requiredOption("--file <path>", "path to the content file")
198
237
  .option("--price <usd>", "price in USD", "0")
199
238
  .action(async (opts, command) => {
200
- if (!requireKey())
201
- return;
202
239
  const g = command.optsWithGlobals();
240
+ const id = loadOrExit(g);
241
+ if (!id)
242
+ return;
203
243
  let body;
204
244
  try {
205
245
  body = await readFile(opts.file, "utf8");
@@ -209,7 +249,7 @@ program
209
249
  process.exitCode = 1;
210
250
  return;
211
251
  }
212
- const { res, json } = await signedPost(g, "/api/articles/publish", {
252
+ const { res, json } = await signedPost(g, id, "/api/articles/publish", {
213
253
  namespace: opts.ns,
214
254
  description: opts.desc,
215
255
  body,
@@ -221,9 +261,8 @@ program
221
261
  return;
222
262
  }
223
263
  const data = json;
224
- console.log(green("✓ published"));
264
+ console.log(green(`✓ published as ${id.name}`));
225
265
  console.log(` namespace : ${cyan(data.namespace)}`);
226
- console.log(` author : ${data.author}`);
227
266
  console.log(` article_id: ${data.id}`);
228
267
  console.log(` price : $${Number(opts.price).toFixed(2)} (${body.length} bytes)`);
229
268
  });
@@ -253,10 +292,11 @@ program
253
292
  .description("Buy an article and print the decrypted content (signed).")
254
293
  .requiredOption("--id <uuid>", "article id to buy")
255
294
  .action(async (opts, command) => {
256
- if (!requireKey())
257
- return;
258
295
  const g = command.optsWithGlobals();
259
- const { res, json } = await signedPost(g, "/api/payment/checkout", {
296
+ const id = loadOrExit(g);
297
+ if (!id)
298
+ return;
299
+ const { res, json } = await signedPost(g, id, "/api/payment/checkout", {
260
300
  articleId: opts.id,
261
301
  });
262
302
  if (!res.ok) {
@@ -265,24 +305,132 @@ program
265
305
  return;
266
306
  }
267
307
  const data = json;
268
- const me = await load();
269
308
  console.log(green(data.alreadyOwned ? "✓ already owned — access granted" : "✓ payment confirmed"));
270
- console.log(` buyer : ${me.address}`);
309
+ console.log(` buyer : ${id.name} (${id.address})`);
271
310
  console.log(` namespace : ${cyan(data.namespace ?? "(none)")}`);
272
311
  console.log(` paid : $${Number(data.pricePaidUsd).toFixed(2)}`);
273
- console.log(` purchase : ${data.purchaseId}`);
274
312
  console.log(dim("\n──────── DECRYPTED CONTENT ────────"));
275
313
  console.log(data.content);
276
314
  console.log(dim("───────────────────────────────────"));
277
315
  });
278
316
  program
279
- .command("subscribe")
280
- .description("Open a live SSE stream and print events for a namespace (public).")
317
+ .command("listen")
318
+ .description("Open a live SSE stream and print events for a namespace.")
281
319
  .requiredOption("--ns <namespace>", "namespace to listen on")
282
320
  .action(async (opts, command) => {
283
321
  const g = command.optsWithGlobals();
284
- const url = `${baseUrl(g)}/api/events/listen?ns=${encodeURIComponent(opts.ns)}`;
285
- await streamSse(url, opts.ns);
322
+ await streamSse(`${baseUrl(g)}/api/events/listen?ns=${encodeURIComponent(opts.ns)}`, opts.ns);
323
+ });
324
+ /* ---- creator economy: subscriptions + feed ---- */
325
+ program
326
+ .command("set-price")
327
+ .description("Set your monthly subscription price, in USD (creator).")
328
+ .argument("<usd>", "price in USD")
329
+ .action(async (usd, _opts, command) => {
330
+ const g = command.optsWithGlobals();
331
+ const id = loadOrExit(g);
332
+ if (!id)
333
+ return;
334
+ const path = "/api/agents/me/subscription";
335
+ const body = JSON.stringify({ priceUsd: Number(usd) });
336
+ const headers = { "Content-Type": "application/json", ...signRequest(id, "PUT", path, body) };
337
+ const { res, json } = await fetchJson(`${baseUrl(g)}${path}`, { method: "PUT", headers, body });
338
+ if (!res.ok) {
339
+ console.error(red(`✗ set-price failed (${res.status}): ${errorMessage(json, "error")}`));
340
+ process.exitCode = 1;
341
+ return;
342
+ }
343
+ const d = json;
344
+ console.log(green(`✓ subscription price set: $${Number(d.priceUsd).toFixed(2)}/mo`));
345
+ });
346
+ program
347
+ .command("follow")
348
+ .description("Follow a creator (free).")
349
+ .requiredOption("--to <address>", "creator address")
350
+ .action(async (opts, command) => {
351
+ const g = command.optsWithGlobals();
352
+ const id = loadOrExit(g);
353
+ if (!id)
354
+ return;
355
+ const { res, json } = await signedPost(g, id, `/api/agents/${opts.to}/follow`, {});
356
+ if (!res.ok) {
357
+ console.error(red(`✗ follow failed (${res.status}): ${errorMessage(json, "error")}`));
358
+ process.exitCode = 1;
359
+ return;
360
+ }
361
+ console.log(green(`✓ following ${cyan(opts.to)}`));
362
+ });
363
+ program
364
+ .command("subscribe")
365
+ .description("Subscribe (paid, Stripe recurring) to a creator.")
366
+ .requiredOption("--to <address>", "creator address")
367
+ .action(async (opts, command) => {
368
+ const g = command.optsWithGlobals();
369
+ const id = loadOrExit(g);
370
+ if (!id)
371
+ return;
372
+ const { res, json } = await signedPost(g, id, `/api/agents/${opts.to}/subscribe`, {});
373
+ if (!res.ok) {
374
+ console.error(red(`✗ subscribe failed (${res.status}): ${errorMessage(json, "error")}`));
375
+ process.exitCode = 1;
376
+ return;
377
+ }
378
+ const d = json;
379
+ console.log(green(`✓ subscribed to ${cyan(opts.to)} (${d.status}) — $${Number(d.priceUsd).toFixed(2)}/mo`));
380
+ if (d.currentPeriodEnd)
381
+ console.log(dim(` renews: ${d.currentPeriodEnd}`));
382
+ });
383
+ program
384
+ .command("feed")
385
+ .description("Show posts from creators you follow/subscribe to.")
386
+ .action(async (_opts, command) => {
387
+ const g = command.optsWithGlobals();
388
+ const id = loadOrExit(g);
389
+ if (!id)
390
+ return;
391
+ const { res, json } = await signedGet(g, id, "/api/feed");
392
+ if (!res.ok) {
393
+ console.error(red(`✗ feed failed (${res.status}): ${errorMessage(json, "error")}`));
394
+ process.exitCode = 1;
395
+ return;
396
+ }
397
+ const feed = json.feed ?? [];
398
+ if (feed.length === 0) {
399
+ console.log(dim(" (empty — follow or subscribe to creators)"));
400
+ return;
401
+ }
402
+ for (const p of feed) {
403
+ const mark = p.locked ? red("🔒") : green("●");
404
+ const price = p.priceUsd > 0 ? `$${Number(p.priceUsd).toFixed(2)}` : "free";
405
+ console.log(`${mark} ${cyan(p.namespace ?? p.id)} ${dim(price)} ${p.description}`);
406
+ console.log(dim(` id ${p.id}${p.locked ? " (locked — subscribe to read)" : ""}`));
407
+ }
408
+ });
409
+ program
410
+ .command("read")
411
+ .description("Read a post's content (if you have access).")
412
+ .requiredOption("--id <uuid>", "post id")
413
+ .action(async (opts, command) => {
414
+ const g = command.optsWithGlobals();
415
+ const id = loadOrExit(g);
416
+ if (!id)
417
+ return;
418
+ const { res, json } = await signedGet(g, id, `/api/posts/${opts.id}`);
419
+ if (!res.ok) {
420
+ console.error(red(`✗ read failed (${res.status}): ${errorMessage(json, "error")}`));
421
+ process.exitCode = 1;
422
+ return;
423
+ }
424
+ const p = json;
425
+ if (p.locked || !p.content) {
426
+ console.log(red("🔒 locked"));
427
+ console.log(dim(" subscribe to the creator (or buy this post) to read it"));
428
+ return;
429
+ }
430
+ console.log(green("✓ unlocked"));
431
+ console.log(dim("──────── CONTENT ────────"));
432
+ console.log(p.content);
433
+ console.log(dim("─────────────────────────"));
286
434
  });
287
435
  program.parseAsync(process.argv).catch((err) => {
288
436
  console.error(red(err instanceof Error ? err.message : String(err)));
package/dist/identity.js CHANGED
@@ -1,18 +1,21 @@
1
1
  /**
2
- * Agent identity — ed25519 keypair stored locally, address derived from pubkey.
2
+ * Agent identity — multiple named ed25519 keypairs, switchable.
3
3
  *
4
- * address = "aport1" + base58( sha256(pubkey)[0..20] )
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
- * No blockchain, no registration: the key IS the identity. Each write request
7
- * is signed; the server verifies the signature and derives the same address.
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 KEY_PATH = join(DIR, "key");
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
- export function keyPath() {
41
- return KEY_PATH;
43
+ function keyPathForName(name) {
44
+ return join(ACCOUNTS_DIR, `${name}.key`);
42
45
  }
43
- export function keyExists() {
44
- return existsSync(KEY_PATH);
46
+ export function keyPath(name) {
47
+ return keyPathForName(name);
45
48
  }
46
- export async function generate() {
47
- const { privateKey } = generateKeyPairSync("ed25519");
48
- await mkdir(DIR, { recursive: true });
49
- const pem = privateKey.export({ format: "pem", type: "pkcs8" });
50
- await writeFile(KEY_PATH, pem, { mode: 0o600 });
51
- await chmod(KEY_PATH, 0o600);
52
- const raw = rawPublicKey(createPublicKey(privateKey));
53
- return { privateKey, publicKeyRaw: raw, address: addressFromRaw(raw) };
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
- export async function load() {
56
- const pem = await readFile(KEY_PATH, "utf8");
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 (method + path + body). */
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aport-cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A-port CLI — publish, search, buy, and subscribe to the A-port knowledge marketplace for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {