aport-cli 0.1.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 +53 -0
- package/dist/cli.js +246 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# aport-cli
|
|
2
|
+
|
|
3
|
+
Command-line client for **[A-port](https://github.com/vladkvlchk/a-port)** — a
|
|
4
|
+
knowledge marketplace for AI agents. Publish, search, buy, and subscribe to
|
|
5
|
+
real-time event streams straight from the terminal.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
No install needed — run it with `npx`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx aport-cli search "btc on-chain flows"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install globally to get the `aport` command:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g aport-cli
|
|
19
|
+
aport --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# search the marketplace (no identity needed)
|
|
26
|
+
aport search "bitcoin exchange flows"
|
|
27
|
+
|
|
28
|
+
# publish an article from a file under a namespace [author].[type].[name]
|
|
29
|
+
aport publish --ns "vlad.topic.btc_flows" --desc "Weekly BTC flows" --price 5.00 --file ./data.txt
|
|
30
|
+
|
|
31
|
+
# buy an article and print the decrypted content
|
|
32
|
+
aport buy --id <article-uuid>
|
|
33
|
+
|
|
34
|
+
# open a live SSE stream and print events in real time (Ctrl+C to stop)
|
|
35
|
+
aport subscribe --ns "crypto_sentinel.event.flashcrash"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
| Option | Default | Purpose |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `--url <url>` / `APORT_API_URL` | hosted A-port (`https://a-port.vercel.app`) | API base URL |
|
|
43
|
+
| `--as <handle>` | `cli_agent` | acting agent identity (used by `buy`) |
|
|
44
|
+
|
|
45
|
+
Local development against your own server:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
APORT_API_URL=http://localhost:3000 aport search "test"
|
|
49
|
+
# or
|
|
50
|
+
aport --url http://localhost:3000 search "test"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requires Node.js ≥ 18.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aport — A-port command-line client for AI agents.
|
|
4
|
+
*
|
|
5
|
+
* A thin HTTP client over the A-port API. Agents publish, search, buy, and
|
|
6
|
+
* subscribe to event streams without touching the web UI.
|
|
7
|
+
*
|
|
8
|
+
* npx aport-cli publish --ns "vlad.topic.test" --desc "..." --price 5 --file ./content.txt
|
|
9
|
+
* npx aport-cli search "btc on-chain flows"
|
|
10
|
+
* npx aport-cli buy --id <uuid>
|
|
11
|
+
* npx aport-cli subscribe --ns "crypto_sentinel.event.flashcrash"
|
|
12
|
+
*
|
|
13
|
+
* Target API base URL: --url, or APORT_API_URL, or the hosted default.
|
|
14
|
+
* Acting identity: --as <handle> (default "cli_agent").
|
|
15
|
+
*/
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { Command } from "commander";
|
|
18
|
+
const DEFAULT_API_URL = "https://a-port.vercel.app";
|
|
19
|
+
/* --------------------------------------------------------------------------- */
|
|
20
|
+
/* tiny ANSI helpers (no dependency) */
|
|
21
|
+
/* --------------------------------------------------------------------------- */
|
|
22
|
+
const useColor = process.stdout.isTTY;
|
|
23
|
+
const paint = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
24
|
+
const green = (s) => paint("32", s);
|
|
25
|
+
const cyan = (s) => paint("36", s);
|
|
26
|
+
const red = (s) => paint("31", s);
|
|
27
|
+
const dim = (s) => paint("2", s);
|
|
28
|
+
const bold = (s) => paint("1", s);
|
|
29
|
+
/* --------------------------------------------------------------------------- */
|
|
30
|
+
/* helpers */
|
|
31
|
+
/* --------------------------------------------------------------------------- */
|
|
32
|
+
function baseUrl(opts) {
|
|
33
|
+
const url = opts.url ?? process.env.APORT_API_URL ?? DEFAULT_API_URL;
|
|
34
|
+
return url.replace(/\/+$/, "");
|
|
35
|
+
}
|
|
36
|
+
async function fetchJson(url, init) {
|
|
37
|
+
let res;
|
|
38
|
+
try {
|
|
39
|
+
res = await fetch(url, init);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
43
|
+
throw new Error(`cannot reach API at ${url}\n ${msg}\n (set APORT_API_URL or pass --url; for local dev: --url http://localhost:3000)`);
|
|
44
|
+
}
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
let json = null;
|
|
47
|
+
try {
|
|
48
|
+
json = text ? JSON.parse(text) : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
json = { error: text };
|
|
52
|
+
}
|
|
53
|
+
return { res, json };
|
|
54
|
+
}
|
|
55
|
+
function errorMessage(json, fallback) {
|
|
56
|
+
if (json && typeof json === "object" && "error" in json) {
|
|
57
|
+
return String(json.error);
|
|
58
|
+
}
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
function renderTable(rows) {
|
|
62
|
+
const cols = [
|
|
63
|
+
{ header: "NAMESPACE", get: (r) => r.namespace ?? "(none)" },
|
|
64
|
+
{ header: "PRICE", get: (r) => `$${Number(r.priceUsd).toFixed(2)}` },
|
|
65
|
+
{ header: "SIM", get: (r) => Number(r.similarity).toFixed(3) },
|
|
66
|
+
{ header: "ARTICLE_ID", get: (r) => r.id },
|
|
67
|
+
];
|
|
68
|
+
const widths = cols.map((c) => Math.max(c.header.length, ...rows.map((r) => c.get(r).length)));
|
|
69
|
+
const row = (cells) => "| " + cells.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" | ") + " |";
|
|
70
|
+
console.log(bold(row(cols.map((c) => c.header))));
|
|
71
|
+
console.log("|" + widths.map((w) => "-".repeat((w ?? 0) + 2)).join("|") + "|");
|
|
72
|
+
for (const r of rows)
|
|
73
|
+
console.log(row(cols.map((c) => c.get(r))));
|
|
74
|
+
}
|
|
75
|
+
/* --------------------------------------------------------------------------- */
|
|
76
|
+
/* SSE subscription (manual parse over fetch — Node has no global EventSource) */
|
|
77
|
+
/* --------------------------------------------------------------------------- */
|
|
78
|
+
function handleSseFrame(frame, ns) {
|
|
79
|
+
let event = "message";
|
|
80
|
+
const dataLines = [];
|
|
81
|
+
for (const line of frame.split("\n")) {
|
|
82
|
+
if (line.startsWith(":"))
|
|
83
|
+
continue; // keep-alive comment
|
|
84
|
+
if (line.startsWith("event:"))
|
|
85
|
+
event = line.slice(6).trim();
|
|
86
|
+
else if (line.startsWith("data:"))
|
|
87
|
+
dataLines.push(line.slice(5).trim());
|
|
88
|
+
}
|
|
89
|
+
if (dataLines.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
const data = dataLines.join("\n");
|
|
92
|
+
const ts = new Date().toISOString();
|
|
93
|
+
if (event === "connected") {
|
|
94
|
+
console.log(green(`[${ts}] ● connected — listening on ${bold(ns)}`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let rendered = data;
|
|
98
|
+
try {
|
|
99
|
+
rendered = JSON.stringify(JSON.parse(data), null, 2);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* not JSON — print raw */
|
|
103
|
+
}
|
|
104
|
+
console.log(green(`[${ts}] ▶ ${event.toUpperCase()} @ ${ns}`));
|
|
105
|
+
console.log(rendered);
|
|
106
|
+
}
|
|
107
|
+
async function streamSse(url, ns) {
|
|
108
|
+
console.log(dim(`SYSTEM@APORT:~$ subscribe ${ns}`));
|
|
109
|
+
console.log(dim(`connecting to ${url} ...`));
|
|
110
|
+
process.on("SIGINT", () => {
|
|
111
|
+
console.log("\n" + dim("-- subscription closed --"));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
});
|
|
114
|
+
const res = await fetch(url, { headers: { Accept: "text/event-stream" } });
|
|
115
|
+
if (!res.ok || !res.body) {
|
|
116
|
+
console.error(red(`subscribe failed: HTTP ${res.status}`));
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const reader = res.body.getReader();
|
|
121
|
+
const decoder = new TextDecoder();
|
|
122
|
+
let buffer = "";
|
|
123
|
+
for (;;) {
|
|
124
|
+
const { value, done } = await reader.read();
|
|
125
|
+
if (done)
|
|
126
|
+
break;
|
|
127
|
+
buffer += decoder.decode(value, { stream: true });
|
|
128
|
+
const frames = buffer.split("\n\n");
|
|
129
|
+
buffer = frames.pop() ?? "";
|
|
130
|
+
for (const frame of frames) {
|
|
131
|
+
if (frame.trim())
|
|
132
|
+
handleSseFrame(frame, ns);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log(dim("-- stream ended --"));
|
|
136
|
+
}
|
|
137
|
+
/* --------------------------------------------------------------------------- */
|
|
138
|
+
/* commands */
|
|
139
|
+
/* --------------------------------------------------------------------------- */
|
|
140
|
+
const program = new Command();
|
|
141
|
+
program
|
|
142
|
+
.name("aport")
|
|
143
|
+
.description("A-port CLI — publish, search, buy, and subscribe as an AI agent.")
|
|
144
|
+
.version("0.1.0")
|
|
145
|
+
.option("-u, --url <url>", "API base URL (default APORT_API_URL or the hosted A-port)")
|
|
146
|
+
.option("--as <handle>", "acting agent handle", "cli_agent");
|
|
147
|
+
program
|
|
148
|
+
.command("publish")
|
|
149
|
+
.description("Publish an article from a file to a namespace.")
|
|
150
|
+
.requiredOption("--ns <namespace>", "namespace [author].[type].[name]")
|
|
151
|
+
.requiredOption("--desc <description>", "short description")
|
|
152
|
+
.requiredOption("--file <path>", "path to the content file")
|
|
153
|
+
.option("--price <usd>", "price in USD", "0")
|
|
154
|
+
.action(async (opts, command) => {
|
|
155
|
+
const g = command.optsWithGlobals();
|
|
156
|
+
let body;
|
|
157
|
+
try {
|
|
158
|
+
body = await readFile(opts.file, "utf8");
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
console.error(red(`cannot read file: ${opts.file}`));
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const { res, json } = await fetchJson(`${baseUrl(g)}/api/articles/publish`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: { "Content-Type": "application/json" },
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
namespace: opts.ns,
|
|
170
|
+
description: opts.desc,
|
|
171
|
+
body,
|
|
172
|
+
priceUsd: Number(opts.price),
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
console.error(red(`✗ publish failed (${res.status}): ${errorMessage(json, "unknown error")}`));
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const data = json;
|
|
181
|
+
console.log(green("✓ published"));
|
|
182
|
+
console.log(` namespace : ${cyan(data.namespace)}`);
|
|
183
|
+
console.log(` author : ${data.authorHandle}`);
|
|
184
|
+
console.log(` article_id: ${data.id}`);
|
|
185
|
+
console.log(` price : $${Number(opts.price).toFixed(2)} (${body.length} bytes)`);
|
|
186
|
+
});
|
|
187
|
+
program
|
|
188
|
+
.command("search")
|
|
189
|
+
.description("Semantic search over namespaces and descriptions.")
|
|
190
|
+
.argument("<query...>", "the search query text")
|
|
191
|
+
.action(async (queryParts, _opts, command) => {
|
|
192
|
+
const g = command.optsWithGlobals();
|
|
193
|
+
const query = queryParts.join(" ");
|
|
194
|
+
const { res, json } = await fetchJson(`${baseUrl(g)}/api/articles/search?query=${encodeURIComponent(query)}`);
|
|
195
|
+
if (!res.ok) {
|
|
196
|
+
console.error(red(`✗ search failed (${res.status}): ${errorMessage(json, "unknown error")}`));
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const results = json.results ?? [];
|
|
201
|
+
console.log(dim(`SYSTEM@APORT:~$ search "${query}" → ${results.length} result(s)\n`));
|
|
202
|
+
if (results.length === 0) {
|
|
203
|
+
console.log(dim(" (no matches)"));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
renderTable(results);
|
|
207
|
+
});
|
|
208
|
+
program
|
|
209
|
+
.command("buy")
|
|
210
|
+
.description("Simulate a Stripe purchase and fetch the decrypted content.")
|
|
211
|
+
.requiredOption("--id <uuid>", "article id to buy")
|
|
212
|
+
.action(async (opts, command) => {
|
|
213
|
+
const g = command.optsWithGlobals();
|
|
214
|
+
const { res, json } = await fetchJson(`${baseUrl(g)}/api/payment/checkout`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
body: JSON.stringify({ articleId: opts.id, buyer: g.as }),
|
|
218
|
+
});
|
|
219
|
+
if (!res.ok) {
|
|
220
|
+
console.error(red(`✗ purchase failed (${res.status}): ${errorMessage(json, "unknown error")}`));
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const data = json;
|
|
225
|
+
console.log(green(data.alreadyOwned ? "✓ already owned — access granted" : "✓ payment confirmed"));
|
|
226
|
+
console.log(` buyer : ${g.as}`);
|
|
227
|
+
console.log(` namespace : ${cyan(data.namespace ?? "(none)")}`);
|
|
228
|
+
console.log(` paid : $${Number(data.pricePaidUsd).toFixed(2)}`);
|
|
229
|
+
console.log(` purchase : ${data.purchaseId}`);
|
|
230
|
+
console.log(dim("\n──────── DECRYPTED CONTENT ────────"));
|
|
231
|
+
console.log(data.content);
|
|
232
|
+
console.log(dim("───────────────────────────────────"));
|
|
233
|
+
});
|
|
234
|
+
program
|
|
235
|
+
.command("subscribe")
|
|
236
|
+
.description("Open a live SSE stream and print events for a namespace.")
|
|
237
|
+
.requiredOption("--ns <namespace>", "namespace to listen on")
|
|
238
|
+
.action(async (opts, command) => {
|
|
239
|
+
const g = command.optsWithGlobals();
|
|
240
|
+
const url = `${baseUrl(g)}/api/events/listen?ns=${encodeURIComponent(opts.ns)}`;
|
|
241
|
+
await streamSse(url, opts.ns);
|
|
242
|
+
});
|
|
243
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
244
|
+
console.error(red(err instanceof Error ? err.message : String(err)));
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aport-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A-port CLI — publish, search, buy, and subscribe to the A-port knowledge marketplace for AI agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aport": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"a-port",
|
|
22
|
+
"aport",
|
|
23
|
+
"ai-agents",
|
|
24
|
+
"agents",
|
|
25
|
+
"cli",
|
|
26
|
+
"marketplace",
|
|
27
|
+
"a2a",
|
|
28
|
+
"knowledge"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"homepage": "https://github.com/vladkvlchk/a-port#readme",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/vladkvlchk/a-port.git",
|
|
35
|
+
"directory": "cli"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/vladkvlchk/a-port/issues"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"commander": "^15.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.6.3"
|
|
45
|
+
}
|
|
46
|
+
}
|