@bolloon/bolloon-agent 0.4.30 → 0.4.31

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.
Files changed (38) hide show
  1. package/dist/agents/agent-gateway.js +5 -0
  2. package/dist/agents/agent-manifest-protocol.js +29 -0
  3. package/dist/agents/economic-policy.js +7 -0
  4. package/dist/agents/gateway-network.js +6 -0
  5. package/dist/agents/local-signer.js +59 -0
  6. package/dist/agents/network-pulse.js +450 -0
  7. package/dist/agents/task-contract.js +322 -0
  8. package/dist/agents/task-inbox.js +267 -0
  9. package/dist/agents/task-transport.js +280 -0
  10. package/dist/agents/x402/transaction-store.js +8 -1
  11. package/dist/cli/commands/agent.js +230 -0
  12. package/dist/cli/commands/index.js +61 -0
  13. package/dist/cli/commands/mcp.js +98 -0
  14. package/dist/cli/commands/network.js +334 -0
  15. package/dist/cli/commands/payment.js +125 -0
  16. package/dist/cli/commands/tasks.js +1236 -0
  17. package/dist/cli/commands/trade.js +229 -0
  18. package/dist/cli/commands/wallet.js +450 -0
  19. package/dist/cli/mcp/bridge.js +105 -0
  20. package/dist/cli/mcp/server.js +185 -0
  21. package/dist/cli/mcp/tools.js +612 -0
  22. package/dist/cli/protocol-envelope.js +346 -0
  23. package/dist/cli-entry.js +141 -17
  24. package/dist/electron-build/agents/agent-gateway.js +5 -0
  25. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  26. package/dist/electron-build/agents/agent-manifest-protocol.js +62 -0
  27. package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
  28. package/dist/electron-build/agents/economic-policy.js +7 -0
  29. package/dist/electron-build/agents/economic-policy.js.map +1 -1
  30. package/dist/electron-build/agents/gateway-network.js +6 -0
  31. package/dist/electron-build/agents/gateway-network.js.map +1 -1
  32. package/dist/electron-build/agents/network-pulse.js +504 -0
  33. package/dist/electron-build/agents/network-pulse.js.map +1 -0
  34. package/dist/electron-build/agents/x402/transaction-store.js +8 -1
  35. package/dist/electron-build/agents/x402/transaction-store.js.map +1 -1
  36. package/dist/web/server.js +29 -0
  37. package/dist/web/task-frame-server.js +211 -0
  38. package/package.json +1 -1
@@ -0,0 +1,280 @@
1
+ /**
2
+ * task-transport.ts — bolloon-task/1 帧 + **现成传输** (P3, 2026-09-21)
3
+ *
4
+ * 任务书要求: `task.send` 必须经**现成传输**把签名请求真发出去, 传输不可用 → 结构化诚实失败。
5
+ * 本模块把三种现存传输包成同一个 `TaskTransport`:
6
+ *
7
+ * ① `http` —— POST `<peer>/api/task/frame` (对端 = `src/web/task-frame-server.ts` 起的节点端点;
8
+ * 与 `agent-delegate-server` 同一套 express + JSON 形状, 真跨进程/跨机 TCP)。
9
+ * ② `iroh` —— `src/network/iroh-transport.ts` 的 `requestResponse` (P2P, 与 `iroh-delegate-transport` 同一条链路;
10
+ * 需要本机 iroh 端点已就绪, 未就绪 → 如实 `TRANSPORT_UNAVAILABLE`)。
11
+ * ③ `gateway` —— `agent-gateway.gatewayCallAgent` 的发现/协商链 (**刻意不传 privateKey**:
12
+ * 这条路径在物理上无法发起任何 x402 付款)。
13
+ *
14
+ * 纪律:
15
+ * · 帧里的信封 (TaskRequest/…) 是**签名过**的; 传输层只加 `signer{ did, publicKeyHex }` (公开材料)
16
+ * 与 `replyTo` / `body` (私有正文, 只走这条直连任务通道)。
17
+ * · 解析严格: protocol 精确相等 + frame 在白名单 + taskId/requestId 与信封一致, 否则拒。
18
+ * · **绝不发起付款**: 本模块没有任何付款调用; 传输失败就是失败, 不重试到"看起来成功"。
19
+ */
20
+ import { TASK_PROTOCOL, } from './task-contract.js';
21
+ export const TASK_FRAME_TYPES = ['task_request', 'task_accept', 'task_reject', 'task_result'];
22
+ /** 对端节点接收任务帧的路径 (与 `task-frame-server` 约定) */
23
+ export const TASK_FRAME_PATH = '/api/task/frame';
24
+ export function buildTaskFrame(frame, envelope, signer, opts = {}) {
25
+ return {
26
+ protocol: TASK_PROTOCOL,
27
+ frame,
28
+ sentAt: opts.now ?? Date.now(),
29
+ signer: { did: String(signer.did || ''), publicKeyHex: String(signer.publicKeyHex || '') },
30
+ ...(opts.replyTo ? { replyTo: opts.replyTo } : {}),
31
+ ...(opts.body ? { body: opts.body } : {}),
32
+ envelope,
33
+ };
34
+ }
35
+ export function serializeFrame(f) {
36
+ return JSON.stringify(f);
37
+ }
38
+ /** 严格解析 (版本/类型/身份/信封标识逐条查; 不合法就说哪一条不合法) */
39
+ export function parseTaskFrame(text) {
40
+ let raw;
41
+ try {
42
+ raw = JSON.parse(String(text));
43
+ }
44
+ catch {
45
+ return { ok: false, error: '不是 JSON' };
46
+ }
47
+ if (!raw || typeof raw !== 'object')
48
+ return { ok: false, error: '帧不是对象' };
49
+ if (raw.protocol !== TASK_PROTOCOL)
50
+ return { ok: false, error: `帧协议版本不对: ${String(raw.protocol)} (要 ${TASK_PROTOCOL})` };
51
+ if (!TASK_FRAME_TYPES.includes(String(raw.frame))) {
52
+ return { ok: false, error: `未知帧类型: ${String(raw.frame)}` };
53
+ }
54
+ const signer = raw.signer;
55
+ if (!signer || typeof signer !== 'object')
56
+ return { ok: false, error: '缺 signer (用来验信封签名)' };
57
+ if (!/^[0-9a-f]{64}$/i.test(String(signer.publicKeyHex || '')))
58
+ return { ok: false, error: 'signer.publicKeyHex 不是 32 字节公钥 hex' };
59
+ const env = raw.envelope;
60
+ if (!env || typeof env !== 'object')
61
+ return { ok: false, error: '缺 envelope' };
62
+ if (env.protocol !== TASK_PROTOCOL)
63
+ return { ok: false, error: `信封协议版本不对: ${String(env.protocol)}` };
64
+ if (!String(env.taskId || '').trim() || !String(env.requestId || '').trim())
65
+ return { ok: false, error: '信封缺 taskId/requestId' };
66
+ if (!String(env.signature || '').trim())
67
+ return { ok: false, error: '信封没有签名 (SIGNATURE_REQUIRED)' };
68
+ return { ok: true, frame: raw };
69
+ }
70
+ // ── ① HTTP (对端 = task-frame-server) ────────────────────────────────────────
71
+ function frameUrl(endpoint) {
72
+ const e = String(endpoint || '').trim().replace(/\/+$/, '');
73
+ if (!e)
74
+ return '';
75
+ return e.endsWith(TASK_FRAME_PATH) ? e : `${e}${TASK_FRAME_PATH}`;
76
+ }
77
+ export function httpTaskTransport(opts = {}) {
78
+ const defaultTimeout = opts.timeoutMs ?? 20_000;
79
+ return {
80
+ kind: 'http',
81
+ async available() {
82
+ const f = opts.fetchImpl ?? globalThis.fetch;
83
+ return typeof f === 'function'
84
+ ? { ok: true }
85
+ : { ok: false, reason: '本机没有 fetch 实现 (Node < 18?)' };
86
+ },
87
+ async send(target, frame, timeoutMs) {
88
+ const t0 = Date.now();
89
+ const url = frameUrl(target);
90
+ if (!url)
91
+ return { ok: false, kind: 'http', target, elapsedMs: 0, error: '目标端点为空 (要 http(s)://…)' };
92
+ const f = opts.fetchImpl ?? fetch;
93
+ try {
94
+ const ctrl = new AbortController();
95
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs ?? defaultTimeout);
96
+ const resp = await f(url, {
97
+ method: 'POST',
98
+ headers: { 'content-type': 'application/json' },
99
+ body: JSON.stringify({ frame }),
100
+ signal: ctrl.signal,
101
+ }).finally(() => clearTimeout(timer));
102
+ const text = await resp.text().catch(() => '');
103
+ if (!resp.ok) {
104
+ return { ok: false, kind: 'http', target: url, elapsedMs: Date.now() - t0, error: `对端 HTTP ${resp.status}`, detail: text.slice(0, 300) };
105
+ }
106
+ let body;
107
+ try {
108
+ body = JSON.parse(text);
109
+ }
110
+ catch {
111
+ return { ok: false, kind: 'http', target: url, elapsedMs: Date.now() - t0, error: '对端回的不是 JSON', detail: text.slice(0, 300) };
112
+ }
113
+ if (body?.ok === false) {
114
+ return { ok: false, kind: 'http', target: url, elapsedMs: Date.now() - t0, error: String(body.error || '对端拒绝了这帧'), detail: body.detail ? String(body.detail).slice(0, 300) : undefined };
115
+ }
116
+ let reply;
117
+ if (body?.reply) {
118
+ const p = parseTaskFrame(typeof body.reply === 'string' ? body.reply : JSON.stringify(body.reply));
119
+ if (!p.ok)
120
+ return { ok: false, kind: 'http', target: url, elapsedMs: Date.now() - t0, error: `对端回的帧不合法: ${p.error}` };
121
+ reply = p.frame;
122
+ }
123
+ return { ok: true, kind: 'http', target: url, elapsedMs: Date.now() - t0, reply, duplicate: body?.duplicate === true, detail: body?.note ? String(body.note).slice(0, 300) : undefined };
124
+ }
125
+ catch (e) {
126
+ const msg = String(e?.message || e);
127
+ const kindHint = /abort/i.test(msg) ? `超时 (${timeoutMs ?? defaultTimeout}ms 内没回)` : msg;
128
+ return { ok: false, kind: 'http', target: url, elapsedMs: Date.now() - t0, error: `对端不可达: ${kindHint.slice(0, 200)}` };
129
+ }
130
+ },
131
+ };
132
+ }
133
+ // ── ② iroh (P2P; 与 iroh-delegate-transport 同一条链路) ──────────────────────
134
+ export function irohTaskTransport(opts = {}) {
135
+ return {
136
+ kind: 'iroh',
137
+ async available() {
138
+ try {
139
+ const mod = await import('../network/iroh-transport.js');
140
+ const t = mod.irohTransport;
141
+ if (!t || typeof t.requestResponse !== 'function')
142
+ return { ok: false, reason: 'irohTransport 不可用' };
143
+ if (typeof t.getNodeId === 'function' && t.getNodeId())
144
+ return { ok: true };
145
+ if (!opts.startIfNeeded)
146
+ return { ok: false, reason: '本机 iroh 端点没起来 (需要先起节点; 或用 --endpoint 走 HTTP 直连)' };
147
+ // 起节点的 secretKey 从 ~/.bolloon/iroh-secret-default.json 来 (跨重启稳定 nodeId), 与既有启动路径一致
148
+ const { loadOrCreateIrohSecret } = await import('./iroh-secret.js');
149
+ const secret = opts.secret || loadOrCreateIrohSecret().secretKey;
150
+ const r = await t.start(secret, false);
151
+ return r?.nodeId ? { ok: true } : { ok: false, reason: 'iroh 端点启动失败 (本机没有可用 iroh 运行时)' };
152
+ }
153
+ catch (e) {
154
+ return { ok: false, reason: `iroh 运行时不可用: ${String(e?.message || e).slice(0, 160)}` };
155
+ }
156
+ },
157
+ async send(target, frame, timeoutMs) {
158
+ const t0 = Date.now();
159
+ const avail = await this.available();
160
+ if (!avail.ok)
161
+ return { ok: false, kind: 'iroh', target, elapsedMs: Date.now() - t0, error: `iroh 传输不可用: ${avail.reason}` };
162
+ try {
163
+ const mod = await import('../network/iroh-transport.js');
164
+ const t = mod.irohTransport;
165
+ const payload = new TextEncoder().encode(serializeFrame(frame));
166
+ const resp = await t.requestResponse(target, 'task_frame', payload, timeoutMs ?? 20_000);
167
+ if (!resp)
168
+ return { ok: false, kind: 'iroh', target, elapsedMs: Date.now() - t0, error: '对端没回 (超时/连不上)' };
169
+ const text = new TextDecoder().decode(resp);
170
+ // 对端直接回一帧, 或回 {ok, reply}
171
+ const direct = parseTaskFrame(text);
172
+ if (direct.ok)
173
+ return { ok: true, kind: 'iroh', target, elapsedMs: Date.now() - t0, reply: direct.frame };
174
+ try {
175
+ const body = JSON.parse(text);
176
+ if (body?.ok === false)
177
+ return { ok: false, kind: 'iroh', target, elapsedMs: Date.now() - t0, error: String(body.error || '对端拒绝') };
178
+ if (body?.reply) {
179
+ const p = parseTaskFrame(typeof body.reply === 'string' ? body.reply : JSON.stringify(body.reply));
180
+ if (p.ok)
181
+ return { ok: true, kind: 'iroh', target, elapsedMs: Date.now() - t0, reply: p.frame, duplicate: body?.duplicate === true };
182
+ }
183
+ }
184
+ catch { /* 落到下面 */ }
185
+ return { ok: false, kind: 'iroh', target, elapsedMs: Date.now() - t0, error: '对端回的内容不是合法帧', detail: text.slice(0, 200) };
186
+ }
187
+ catch (e) {
188
+ return { ok: false, kind: 'iroh', target, elapsedMs: Date.now() - t0, error: `iroh 发送失败: ${String(e?.message || e).slice(0, 200)}` };
189
+ }
190
+ },
191
+ };
192
+ }
193
+ // ── ③ gateway (agent-gateway 的既有发现/协商链; **不带私钥 → 不可能付款**) ────
194
+ export function gatewayTaskTransport() {
195
+ return {
196
+ kind: 'gateway',
197
+ async available() {
198
+ try {
199
+ const { getAgentRegistry } = await import('./agent-registry.js');
200
+ const reg = getAgentRegistry();
201
+ return reg ? { ok: true } : { ok: false, reason: 'registry 不可用' };
202
+ }
203
+ catch (e) {
204
+ return { ok: false, reason: String(e?.message || e).slice(0, 120) };
205
+ }
206
+ },
207
+ async send(target, frame, timeoutMs) {
208
+ const t0 = Date.now();
209
+ try {
210
+ const { gatewayCallAgent } = await import('./agent-gateway.js');
211
+ let handled = false;
212
+ // ★ 刻意不传 privateKey: 这条路径无法发起 x402 付款 (policy.recordSpend 也不执行)
213
+ const r = await gatewayCallAgent({
214
+ task: serializeFrame(frame),
215
+ budget: Number.MAX_SAFE_INTEGER, // 预算已由任务信封 (budget.maxAmount) 约束; 这里不另设闸
216
+ capability: undefined,
217
+ url: target,
218
+ });
219
+ handled = true;
220
+ if (!r.success) {
221
+ return { ok: false, kind: 'gateway', target, elapsedMs: Date.now() - t0, error: r.error || 'gateway 调用失败', detail: `decision=${r.decision || 'n/a'} paid=false handled=${handled}` };
222
+ }
223
+ const out = String(r.output || '');
224
+ try {
225
+ const body = JSON.parse(out);
226
+ // out = {ok, reply} 形状 (对端 task-frame-server) 或直接是帧
227
+ if (body?.reply) {
228
+ const p = parseTaskFrame(typeof body.reply === 'string' ? body.reply : JSON.stringify(body.reply));
229
+ if (p.ok)
230
+ return { ok: true, kind: 'gateway', target, elapsedMs: Date.now() - t0, reply: p.frame, duplicate: body?.duplicate === true };
231
+ }
232
+ }
233
+ catch { /* 交给下面 */ }
234
+ const direct = parseTaskFrame(out);
235
+ if (direct.ok)
236
+ return { ok: true, kind: 'gateway', target, elapsedMs: Date.now() - t0, reply: direct.frame };
237
+ return { ok: false, kind: 'gateway', target, elapsedMs: Date.now() - t0, error: 'gateway 执行结果不是合法任务帧', detail: out.slice(0, 200) };
238
+ }
239
+ catch (e) {
240
+ return { ok: false, kind: 'gateway', target, elapsedMs: Date.now() - t0, error: `gateway 传输异常: ${String(e?.message || e).slice(0, 200)}` };
241
+ }
242
+ },
243
+ };
244
+ }
245
+ /**
246
+ * 不给 `--endpoint`/`--peer` 时: 用 registry 按 capability 找 provider 的 endpoint。
247
+ * 找不到 → null (调用方如实报 CAPABILITY_NOT_FOUND / NETWORK_NOT_JOINED, 不猜地址)。
248
+ */
249
+ export async function resolveTaskTarget(capability, opts = {}) {
250
+ let registry = opts.registry;
251
+ if (!registry) {
252
+ const { getAgentRegistry } = await import('./agent-registry.js');
253
+ registry = getAgentRegistry();
254
+ }
255
+ const ready = registry?.ready === true;
256
+ let services = [];
257
+ try {
258
+ services = capability ? await registry.discover(capability) : await registry.list();
259
+ }
260
+ catch {
261
+ services = [];
262
+ }
263
+ const withEndpoint = services.find((s) => typeof s?.endpoint === 'string' && s.endpoint.trim());
264
+ if (!withEndpoint)
265
+ return { target: null, registryReady: ready, candidates: services.length };
266
+ return {
267
+ target: {
268
+ target: String(withEndpoint.endpoint).trim(),
269
+ kind: 'http',
270
+ agentId: withEndpoint.agentId,
271
+ providerDid: withEndpoint.agentId || undefined,
272
+ wallet: withEndpoint.wallet,
273
+ priceAmount: withEndpoint.service?.price?.amount,
274
+ priceCurrency: withEndpoint.service?.price?.currency,
275
+ source: 'registry.discover(capability) → service.endpoint',
276
+ },
277
+ registryReady: ready,
278
+ candidates: services.length,
279
+ };
280
+ }
@@ -197,7 +197,14 @@ export async function updateTransaction(transactionId, patch, home = os.homedir(
197
197
  ...rest,
198
198
  events: ev2 ? [...(rec.events || []), event(ev2.kind, ev2.detail)] : (rec.events || []),
199
199
  };
200
- return await saveTransaction(next, home);
200
+ const saved = await saveTransaction(next, home);
201
+ // ★ 公开观察层: 真实交易活动 → 脉冲事件 (fire-and-forget; 统计失败绝不影响交易主路径)
202
+ try {
203
+ const np = await import('../network-pulse.js');
204
+ void np.emitTradePulse(rec, saved, home);
205
+ }
206
+ catch { /* noop */ }
207
+ return saved;
201
208
  }
202
209
  export async function setTransactionStatus(transactionId, status, detail, home = os.homedir()) {
203
210
  return updateTransaction(transactionId, { status, ...(status === 'settled' ? { settledAt: new Date().toISOString() } : {}), ...(status === 'delivered' ? { deliveredAt: new Date().toISOString() } : {}), ...(status === 'verified' ? { verifiedAt: new Date().toISOString() } : {}), event: { kind: `status:${status}`, detail } }, home);
@@ -0,0 +1,230 @@
1
+ /**
2
+ * agent.ts — P3 `bolloon agent register|manifest|discover|inspect`
3
+ *
4
+ * 全部是现有服务的薄包装:
5
+ * register → `agent-gateway.gatewayRegisterAgent()` (warm registry + 写服务声明, 落 ~/.bolloon/agent-registry.json)
6
+ * `--manifest` 时另调 `agent-manifest-protocol.setLocalManifest()` (进程内, 不落盘 —— 如实标注)
7
+ * manifest → `agent-manifest-protocol.getLocalManifest()`
8
+ * discover → `agent-registry.discover(q)` (OrbitDB 优先, 失败回退本地文件)
9
+ * inspect → `agent-registry.list()` 按 agentId 命中 / 远端 manifest 缓存 (`getRemoteManifests`)
10
+ *
11
+ * 找不到能力 → §3 `CAPABILITY_NOT_FOUND` (next_action `redefine_capability`); **绝不**因此发起付款 (§9 第 3 条)。
12
+ */
13
+ import { okEnvelope, failEnvelope, line, title, hint, plain, opt, has, } from '../protocol-envelope.js';
14
+ export const AGENT_USAGE = `
15
+ ${title('bolloon agent')}
16
+ bolloon agent discover <能力> [--json] 按能力发现服务 (registry, OrbitDB 优先/本地回退)
17
+ bolloon agent register --capability <能力> [--name 名] [--price 0.05] [--per query] [--description 说明] [--wallet 0x..]
18
+ bolloon agent manifest [--json] 本进程内的 manifest (agents/capabilities)
19
+ bolloon agent inspect <agentId|能力> [--json] 看某个 agent 的声明 (钱包/服务/能力/信誉)
20
+
21
+ 选项: --json · --quiet · --request-id <id> · --timeout <ms>
22
+ `;
23
+ /** registry 预热 (有界): CLI 一次性命令里不许无限等 OrbitDB */
24
+ async function warmBounded(ms) {
25
+ try {
26
+ const { warmAgentRegistry } = await import('../../agents/agent-registry.js');
27
+ return await Promise.race([
28
+ warmAgentRegistry().then((v) => !!v),
29
+ new Promise((resolve) => setTimeout(() => resolve(false), ms)),
30
+ ]);
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ async function localDid() {
37
+ const { getUserOwnerDid } = await import('../../agents/agent-identity.js');
38
+ const fromUser = getUserOwnerDid();
39
+ if (fromUser)
40
+ return fromUser;
41
+ try {
42
+ const { getGatewayJoinState } = await import('../../agents/gateway-join.js');
43
+ const st = await getGatewayJoinState();
44
+ return String(st?.did || '');
45
+ }
46
+ catch {
47
+ return '';
48
+ }
49
+ }
50
+ export async function agentCommand(flags) {
51
+ const sub = flags.positionals[0];
52
+ switch (sub) {
53
+ case 'register': return agentRegister(flags);
54
+ case 'manifest': return agentManifest(flags);
55
+ case 'discover': return agentDiscover(flags);
56
+ case 'inspect': return agentInspect(flags);
57
+ default:
58
+ return {
59
+ envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 agent 子命令: ${sub}` : '缺少 agent 子命令', { usage: plain(AGENT_USAGE.trim()) }, [], 'needs_human'),
60
+ human: AGENT_USAGE,
61
+ };
62
+ }
63
+ }
64
+ // ── agent discover ──────────────────────────────────────────────────────────
65
+ async function agentDiscover(flags) {
66
+ const q = flags.positionals[1] || opt(flags, '--capability') || '';
67
+ const ready = await warmBounded(flags.timeoutMs ?? 8_000);
68
+ const { getAgentRegistry } = await import('../../agents/agent-registry.js');
69
+ const reg = getAgentRegistry(await localDid() || 'local');
70
+ const hits = await reg.discover(q);
71
+ const data = {
72
+ query: q,
73
+ count: hits.length,
74
+ registryReady: ready,
75
+ services: hits.map((s) => ({
76
+ agentId: s.agentId, name: s.name, wallet: s.wallet,
77
+ capability: s.service?.name, description: s.service?.description,
78
+ price: s.service?.price, capabilities: s.capabilities || [],
79
+ })),
80
+ };
81
+ if (!hits.length) {
82
+ return {
83
+ // §3 CAPABILITY_NOT_FOUND: ✅可重试 (**只重查发现**, 绝不因此重发付款)
84
+ envelope: failEnvelope('CAPABILITY_NOT_FOUND', q ? `registry 里找不到能力/关键字 '${q}'` : 'registry 里没有任何服务声明', data, [], 'redefine_capability'),
85
+ human: `${title('bolloon agent discover')}\n${line('查询', q || '(全部)')}\n 没找到 (registry ready=${ready})\n\n${hint('下一步: 换关键词重查 (发现是只读的, 重试安全); 或让对方先声明能力 —— 查不到 ≠ 网络上没有, 更不要因此付款')}`,
86
+ };
87
+ }
88
+ return {
89
+ envelope: okEnvelope('OK', `找到 ${hits.length} 个匹配 '${q || '全部'}' 的服务`, data, hits.map((s) => s.agentId), null),
90
+ human: [
91
+ title(`bolloon agent discover ${q || '(全部)'}`),
92
+ line('命中', `${hits.length} 个 (registry ready=${ready})`),
93
+ ...hits.slice(0, 20).map((s) => `\n ${s.name} [${s.service?.name}] ${s.service?.price?.amount || '?'} ${s.service?.price?.currency || ''}/${s.service?.price?.per || ''}\n agentId: ${s.agentId}\n wallet: ${s.wallet || '(无)'}`),
94
+ ].join('\n'),
95
+ };
96
+ }
97
+ // ── agent register ──────────────────────────────────────────────────────────
98
+ async function agentRegister(flags) {
99
+ const capability = opt(flags, '--capability') || flags.positionals[1];
100
+ if (!capability) {
101
+ return {
102
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 --capability (要声明什么能力)', { usage: plain(AGENT_USAGE.trim()) }, [], 'needs_human'),
103
+ human: `${AGENT_USAGE}\n${hint('示例: bolloon agent register --capability research --price 0.05 --per query --name 我的研究体')}`,
104
+ };
105
+ }
106
+ const did = await localDid();
107
+ if (!did) {
108
+ return {
109
+ envelope: failEnvelope('INVALID_ARGUMENT', '本机还没有 DID 身份, 无法声明能力', { did: '' }, [], 'needs_human'),
110
+ human: `${title('bolloon agent register')}\n 本机还没有 DID 身份\n\n${hint('下一步: bolloon setup (生成 ~/.bolloon/identity/user.json) 或 bolloon network join 入网')}`,
111
+ };
112
+ }
113
+ const name = opt(flags, '--name') || 'bolloon-agent';
114
+ const wallet = opt(flags, '--wallet') || '';
115
+ const { gatewayRegisterAgent } = await import('../../agents/agent-gateway.js');
116
+ const r = await gatewayRegisterAgent({
117
+ capability,
118
+ price: opt(flags, '--price') || '0',
119
+ currency: opt(flags, '--currency') || 'USDC',
120
+ per: opt(flags, '--per') || 'query',
121
+ description: opt(flags, '--description'),
122
+ wallet,
123
+ endpoint: opt(flags, '--endpoint'),
124
+ }, { did, name, wallet });
125
+ // `--manifest`: 额外登记进程内 manifest (agent-manifest-protocol **不落盘**, 如实标注)
126
+ let manifest = null;
127
+ if (has(flags, '--manifest')) {
128
+ const { setLocalManifest, getLocalManifest } = await import('../../agents/agent-manifest-protocol.js');
129
+ setLocalManifest({ ownerName: name, ownerPublicKey: did, agents: [{ id: `${name}-main`, name, capabilities: [capability], status: 'active' }] });
130
+ const m = getLocalManifest();
131
+ manifest = { registered: (m.agents || []).length > 0, persisted: false, agents: (m.agents || []).map((a) => a.id) };
132
+ }
133
+ const data = { agentId: did, name, capability, wallet: wallet || null, registryPath: '~/.bolloon/agent-registry.json', manifest };
134
+ if (!r.ok) {
135
+ return {
136
+ envelope: failEnvelope('INTERNAL_ERROR', `服务声明没写成功: ${r.error || '未知原因'}`, data, [did], 'needs_human'),
137
+ human: `${title('bolloon agent register')}\n ✗ 没写成功: ${r.error || '未知原因'}`,
138
+ };
139
+ }
140
+ return {
141
+ envelope: okEnvelope('AGENT_REGISTERED', `已声明能力 '${capability}' (agentId=${did})`, data, [did], null),
142
+ human: `${title('bolloon agent register')}\n${line('agentId', did)}\n${line('名称', name)}\n${line('能力', capability)}\n${line('价格', `${opt(flags, '--price') || '0'} ${opt(flags, '--currency') || 'USDC'}/${opt(flags, '--per') || 'query'}`)}\n${line('账号钱包', wallet || '(未给)')}${manifest ? `\n${line('manifest', manifest.registered ? '本进程已登记 (不落盘)' : '登记后读回为空')}` : ''}\n\n${hint('下一步: bolloon agent discover ' + capability + ' 自查能不能被发现')}`,
143
+ };
144
+ }
145
+ // ── agent manifest ──────────────────────────────────────────────────────────
146
+ async function agentManifest(flags) {
147
+ const { getLocalManifest, getRemoteManifests } = await import('../../agents/agent-manifest-protocol.js');
148
+ const m = getLocalManifest();
149
+ const remote = getRemoteManifests();
150
+ const data = {
151
+ scope: 'in-process',
152
+ persisted: false,
153
+ ownerName: m.ownerName || '',
154
+ ownerPublicKey: m.ownerPublicKey || '',
155
+ publishedAt: m.publishedAt || 0,
156
+ agents: (m.agents || []).map((a) => ({ id: a.id, name: a.name, capabilities: a.capabilities, status: a.status })),
157
+ remoteManifests: remote.map((r) => ({ ownerPublicKey: r.ownerPublicKey, ownerName: r.ownerName, agents: (r.agents || []).length })),
158
+ note: 'local manifest 是**进程内**结构 (agent-manifest-protocol 不落盘): 一次性 CLI 命令里读不到 = 这个进程还没登记过, 不等于本机没注册过 (注册发生在运行中的 Runtime 进程内)',
159
+ };
160
+ return {
161
+ envelope: okEnvelope('OK', `本进程 manifest: agents=${data.agents.length}, 远端缓存=${remote.length} 份`, data, data.ownerPublicKey ? [data.ownerPublicKey] : [], null),
162
+ human: [
163
+ title('bolloon agent manifest (本进程)'),
164
+ line('ownerName', data.ownerName || '(空)'),
165
+ line('ownerDid', data.ownerPublicKey || '(空)'),
166
+ line('agents', data.agents.length),
167
+ ...data.agents.map((a) => ` · ${a.id} [${a.capabilities.join(',')}] ${a.status}`),
168
+ line('远端 manifest 缓存', remote.length),
169
+ `\n ${data.note}`,
170
+ ].join('\n'),
171
+ };
172
+ }
173
+ // ── agent inspect ───────────────────────────────────────────────────────────
174
+ async function agentInspect(flags) {
175
+ const target = flags.positionals[1] || opt(flags, '--capability') || '';
176
+ if (!target) {
177
+ return {
178
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少要查看的 agentId 或能力名', { usage: plain(AGENT_USAGE.trim()) }, [], 'needs_human'),
179
+ human: AGENT_USAGE,
180
+ };
181
+ }
182
+ const ready = await warmBounded(flags.timeoutMs ?? 8_000);
183
+ const { getAgentRegistry } = await import('../../agents/agent-registry.js');
184
+ const reg = getAgentRegistry((await localDid()) || 'local');
185
+ const all = await reg.list();
186
+ const byId = all.find((s) => s.agentId === target);
187
+ const byCap = byId ? undefined : all.find((s) => (s.capabilities || []).includes(target) || s.service?.name === target);
188
+ const hit = byId || byCap;
189
+ if (hit) {
190
+ return {
191
+ envelope: okEnvelope('OK', `找到 ${hit.agentId}`, {
192
+ registryReady: ready, matchedBy: byId ? 'agentId' : 'capability',
193
+ agent: {
194
+ agentId: hit.agentId, name: hit.name, wallet: hit.wallet,
195
+ service: hit.service, capabilities: hit.capabilities || [],
196
+ endpoint: hit.endpoint || null, reputation: hit.reputation || null,
197
+ registeredAt: hit.registeredAt || null, updatedAt: hit.updatedAt || null,
198
+ },
199
+ }, [hit.agentId], null),
200
+ human: [
201
+ title(`bolloon agent inspect ${target}`),
202
+ line('agentId', hit.agentId),
203
+ line('名称', hit.name),
204
+ line('能力', (hit.capabilities || []).join(', ')),
205
+ line('服务', `${hit.service?.name} — ${hit.service?.description || ''}`),
206
+ line('价格', `${hit.service?.price?.amount} ${hit.service?.price?.currency}/${hit.service?.price?.per}`),
207
+ line('收款钱包', hit.wallet || '(未声明)'),
208
+ line('信誉', hit.reputation ? `${hit.reputation.score} (${hit.reputation.tasks} 次)` : '(无记录)'),
209
+ line('更新时间', hit.updatedAt || '(无)'),
210
+ ].join('\n'),
211
+ };
212
+ }
213
+ // 远端 manifest 缓存兜底 (对方节点握手时换来的人)
214
+ const { getRemoteManifests } = await import('../../agents/agent-manifest-protocol.js');
215
+ const remoteHit = getRemoteManifests().find((r) => r.ownerPublicKey === target || (r.agents || []).some((a) => a.id === target || (a.capabilities || []).includes(target)));
216
+ if (remoteHit) {
217
+ return {
218
+ envelope: okEnvelope('OK', `在远端 manifest 缓存里找到 ${target}`, {
219
+ registryReady: ready, matchedBy: 'remote-manifest',
220
+ owner: { ownerPublicKey: remoteHit.ownerPublicKey, ownerName: remoteHit.ownerName },
221
+ agents: (remoteHit.agents || []).map((a) => ({ id: a.id, name: a.name, capabilities: a.capabilities, status: a.status })),
222
+ }, [remoteHit.ownerPublicKey], null),
223
+ human: `${title(`bolloon agent inspect ${target}`)}\n${line('来源', '远端 manifest 缓存 (P2P 握手换来)')}\n${line('owner', `${remoteHit.ownerName} ${remoteHit.ownerPublicKey}`)}\n${(remoteHit.agents || []).map((a) => ` · ${a.id} [${(a.capabilities || []).join(',')}]`).join('\n')}`,
224
+ };
225
+ }
226
+ return {
227
+ envelope: failEnvelope('CAPABILITY_NOT_FOUND', `registry 与远端缓存里都没有 '${target}'`, { registryReady: ready, query: target, knownAgents: all.map((s) => s.agentId) }, [], 'redefine_capability'),
228
+ human: `${title(`bolloon agent inspect ${target}`)}\n 没找到 (registry ready=${ready}, 本机已知 ${all.length} 个服务)\n\n${hint('下一步: bolloon agent discover <关键字> 换词重查 (只读, 重试安全)')}`,
229
+ };
230
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * index.ts — P3 命令组调度 (network / agent / task / wallet / payment / trade)
3
+ *
4
+ * 每个命令组只做两件事: 解析全局选项 (`--json`/`--quiet`/`--request-id`/`--timeout`),
5
+ * 然后把活交给对应模块里的**薄包装**。业务逻辑一律不在这里, 也不在命令模块里重实现。
6
+ */
7
+ import { parseFlags, runCommand } from '../protocol-envelope.js';
8
+ import { networkCommand } from './network.js';
9
+ import { agentCommand } from './agent.js';
10
+ import { taskCommand } from './tasks.js';
11
+ import { walletCommand } from './wallet.js';
12
+ import { paymentCommand } from './payment.js';
13
+ import { tradeCommand } from './trade.js';
14
+ /** P3 命令组名 (cli-entry 的 parseArgs 也认这些) */
15
+ export const SERVICE_GROUPS = ['network', 'agent', 'task', 'wallet', 'payment', 'trade'];
16
+ export function isServiceGroup(mode) {
17
+ return SERVICE_GROUPS.includes(mode);
18
+ }
19
+ /** 模式 → 命令函数 (唯一映射表: CLI 与 MCP 适配层都**只**从这里取, 不许各自 if-else) */
20
+ export const GROUP_COMMANDS = {
21
+ network: networkCommand,
22
+ agent: agentCommand,
23
+ task: taskCommand,
24
+ wallet: walletCommand,
25
+ payment: paymentCommand,
26
+ trade: tradeCommand,
27
+ };
28
+ export const GROUPS_HELP = `
29
+ 命令组 (P3, 统一信封 { ok, code, message, data, evidence, next_action }):
30
+
31
+ bolloon network status | init | join [link] | peers | leave(未实现)
32
+ bolloon agent discover | register | manifest | inspect
33
+ bolloon task list | status | result | retry | run | send|inbox|accept|reject|complete|cancel(未实现)
34
+ bolloon wallet status | policy | set-policy
35
+ bolloon payment pending | approve | reject
36
+ bolloon trade list | show | events | reconcile
37
+
38
+ 全局选项 (全命令组有效):
39
+ --json 输出 §2 冻结信封 (失败也结构化: ok:false + code + next_action)
40
+ --quiet 只输出结果 (payload 的 JSON)
41
+ --request-id <id> 幂等键原样透传 (用在真实做幂等查找/派生的命令上)
42
+ --timeout <ms> 硬超时; 超时给 code=TIMEOUT (不是"打印一行错误就退出")
43
+
44
+ 判据永远是 ok / code, **不是**退出码; 也不要把 paid / delivered 读成成功
45
+ (local-dev 永远不是链上结算 —— 见 docs/wiki/access-protocol-v1.md §5)。
46
+ `;
47
+ /** 跑一个命令组, 返回进程退出码 (0=ok, 1=失败) */
48
+ export async function runServiceGroup(mode, args) {
49
+ const flags = parseFlags(args);
50
+ switch (mode) {
51
+ case 'network': return runCommand(flags, networkCommand);
52
+ case 'agent': return runCommand(flags, agentCommand);
53
+ case 'task': return runCommand(flags, taskCommand);
54
+ case 'wallet': return runCommand(flags, walletCommand);
55
+ case 'payment': return runCommand(flags, paymentCommand);
56
+ case 'trade': return runCommand(flags, tradeCommand);
57
+ default:
58
+ console.log(GROUPS_HELP);
59
+ return 1;
60
+ }
61
+ }