@bolloon/bolloon-agent 0.4.29 → 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 (83) 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/contacts/chain.js +579 -0
  4. package/dist/agents/contacts/cli.js +158 -0
  5. package/dist/agents/contacts/consent.js +124 -0
  6. package/dist/agents/contacts/grant-payload.js +45 -0
  7. package/dist/agents/contacts/grants.js +451 -0
  8. package/dist/agents/contacts/policy.js +264 -0
  9. package/dist/agents/contacts/preview-types.js +6 -0
  10. package/dist/agents/contacts/providers.js +341 -0
  11. package/dist/agents/contacts/store.js +326 -0
  12. package/dist/agents/contacts/tools.js +154 -0
  13. package/dist/agents/contacts/types.js +148 -0
  14. package/dist/agents/economic-policy.js +7 -0
  15. package/dist/agents/gateway-network.js +6 -0
  16. package/dist/agents/local-signer.js +59 -0
  17. package/dist/agents/network-pulse.js +450 -0
  18. package/dist/agents/pi-sdk.js +10 -0
  19. package/dist/agents/task-contract.js +322 -0
  20. package/dist/agents/task-inbox.js +267 -0
  21. package/dist/agents/task-transport.js +280 -0
  22. package/dist/agents/x402/transaction-store.js +8 -1
  23. package/dist/cli/commands/agent.js +230 -0
  24. package/dist/cli/commands/index.js +61 -0
  25. package/dist/cli/commands/mcp.js +98 -0
  26. package/dist/cli/commands/network.js +334 -0
  27. package/dist/cli/commands/payment.js +125 -0
  28. package/dist/cli/commands/tasks.js +1236 -0
  29. package/dist/cli/commands/trade.js +229 -0
  30. package/dist/cli/commands/wallet.js +450 -0
  31. package/dist/cli/mcp/bridge.js +105 -0
  32. package/dist/cli/mcp/server.js +185 -0
  33. package/dist/cli/mcp/tools.js +612 -0
  34. package/dist/cli/protocol-envelope.js +346 -0
  35. package/dist/cli-entry.js +141 -17
  36. package/dist/electron-build/agents/agent-gateway.js +5 -0
  37. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  38. package/dist/electron-build/agents/agent-manifest-protocol.js +62 -0
  39. package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
  40. package/dist/electron-build/agents/contacts/chain.js +619 -0
  41. package/dist/electron-build/agents/contacts/chain.js.map +1 -0
  42. package/dist/electron-build/agents/contacts/consent.js +163 -0
  43. package/dist/electron-build/agents/contacts/consent.js.map +1 -0
  44. package/dist/electron-build/agents/contacts/grant-payload.js +52 -0
  45. package/dist/electron-build/agents/contacts/grant-payload.js.map +1 -0
  46. package/dist/electron-build/agents/contacts/grants.js +500 -0
  47. package/dist/electron-build/agents/contacts/grants.js.map +1 -0
  48. package/dist/electron-build/agents/contacts/policy.js +271 -0
  49. package/dist/electron-build/agents/contacts/policy.js.map +1 -0
  50. package/dist/electron-build/agents/contacts/preview-types.js +8 -0
  51. package/dist/electron-build/agents/contacts/preview-types.js.map +1 -0
  52. package/dist/electron-build/agents/contacts/providers.js +384 -0
  53. package/dist/electron-build/agents/contacts/providers.js.map +1 -0
  54. package/dist/electron-build/agents/contacts/store.js +370 -0
  55. package/dist/electron-build/agents/contacts/store.js.map +1 -0
  56. package/dist/electron-build/agents/contacts/tools.js +159 -0
  57. package/dist/electron-build/agents/contacts/tools.js.map +1 -0
  58. package/dist/electron-build/agents/contacts/types.js +166 -0
  59. package/dist/electron-build/agents/contacts/types.js.map +1 -0
  60. package/dist/electron-build/agents/economic-policy.js +7 -0
  61. package/dist/electron-build/agents/economic-policy.js.map +1 -1
  62. package/dist/electron-build/agents/external-events.js.map +1 -1
  63. package/dist/electron-build/agents/gateway-network.js +6 -0
  64. package/dist/electron-build/agents/gateway-network.js.map +1 -1
  65. package/dist/electron-build/agents/goal-store.js.map +1 -1
  66. package/dist/electron-build/agents/network-pulse.js +504 -0
  67. package/dist/electron-build/agents/network-pulse.js.map +1 -0
  68. package/dist/electron-build/agents/pi-sdk.js +10 -0
  69. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  70. package/dist/electron-build/agents/x402/transaction-store.js +8 -1
  71. package/dist/electron-build/agents/x402/transaction-store.js.map +1 -1
  72. package/dist/electron-build/security/tool-gate.js +4 -0
  73. package/dist/electron-build/security/tool-gate.js.map +1 -1
  74. package/dist/index.js +30 -0
  75. package/dist/security/tool-gate.js +4 -0
  76. package/dist/web/mobile-contacts.js +329 -0
  77. package/dist/web/mobile-core.js +481 -15
  78. package/dist/web/mobile.html +32 -0
  79. package/dist/web/mobile.js +139 -0
  80. package/dist/web/routes-contacts.js +420 -0
  81. package/dist/web/server.js +34 -0
  82. package/dist/web/task-frame-server.js +211 -0
  83. package/package.json +1 -1
@@ -0,0 +1,346 @@
1
+ /**
2
+ * protocol-envelope.ts — P3 统一输出层 (P1 §2 冻结 JSON 信封 + §3 错误码 + §2.3 next_action 词表)
3
+ *
4
+ * 权威口径: `docs/wiki/access-protocol-v1.md`。本文件**只做四件事**, 不含任何业务逻辑:
5
+ * ① 全局选项解析: `--json` / `--quiet` / `--request-id <id>` / `--timeout <ms>`
6
+ * ② 信封构造: `{ ok, code, message, data, evidence, next_action }` (字段顺序与 §2 一致)
7
+ * ③ 输出: `--json` 信封 · `--quiet` 只出结果 · 默认人类可读 (既有体验是基线, 不许坏)
8
+ * ④ 兜底: 超时/异常也必须是**结构化失败** (§2 硬规则 1); 私钥类字段一律不进 stdout
9
+ *
10
+ * 2026-09-21 (P3): 此前 `src/` 里**没有任何** §3 错误码常量 (P1 §0 表逐条核对过) —— 本文件把它们落成常量。
11
+ *
12
+ * 三条不许破的规矩 (P1 §2 硬规则):
13
+ * 1. 成功和失败都必须结构化 —— 失败也要 `ok:false` + `code` + `next_action`
14
+ * 2. "已付款" ≠ "任务成功" —— `paid`/`delivered` 都不是成功; 成功判据是 §5.2 (任务层: state=verified ∧ 事实∈{fully_settled,payment_verified})
15
+ * 3. 客户端必须忽略未知 `code` —— 所以本文件新增的码是**追加**, 不改任何冻结码的含义
16
+ */
17
+ import { AUDIT_FORBIDDEN_KEYS } from '../agents/task-contract.js';
18
+ // ── §3 冻结错误码 (字面量与 P1 表逐条一致; 改含义 = 必须 bump 协议版本) ──────────
19
+ /** P1 §3 表里的码 (含成功码)。这些的含义**冻结**, 不许在 CLI 层重新解释。 */
20
+ export const FROZEN_CODES = [
21
+ 'TASK_SUBMITTED', 'TASK_ACCEPTED', 'TASK_COMPLETED', 'TASK_VERIFIED',
22
+ 'PAYMENT_REQUIRED', 'PAYMENT_PENDING', 'POLICY_DENIED', 'BUDGET_EXCEEDED',
23
+ 'NETWORK_NOT_JOINED', 'CAPABILITY_NOT_FOUND', 'RESULT_UNVERIFIED',
24
+ 'PROTOCOL_VERSION_UNSUPPORTED', 'PAYMENT_UNCERTAIN', 'DUPLICATE_REQUEST',
25
+ 'TASK_TRANSITION_REJECTED', 'SIGNATURE_REQUIRED', 'SIGNATURE_INVALID',
26
+ 'DEADLINE_EXPIRED', 'WALLET_UNAVAILABLE', 'AGENT_NOT_AUTHORIZED',
27
+ 'LOCAL_DEV_NOT_CHAIN', 'DISPUTE_OPEN', 'DELIVERY_FAILED',
28
+ ];
29
+ /**
30
+ * P3 新增码 (P1 §1.3「新增错误码不 bump 版本」允许; 老客户端按 `ok:false` + `next_action` 处理, 不崩不猜)。
31
+ * 只加**表里确实没有对应语义**的: 命令组自身的成败、参数错、找不到、超时、未实现。
32
+ */
33
+ export const CLI_CODES = [
34
+ // 命令组成功码
35
+ 'OK', 'NETWORK_JOINED', 'NETWORK_NODE_READY', 'AGENT_REGISTERED',
36
+ 'POLICY_UPDATED', 'PAYMENT_APPROVED', 'PAYMENT_REJECTED', 'RECOVERY_PLANNED',
37
+ 'WALLET_SIGNED', 'TASK_DELIVERED',
38
+ // 任务态里 §3 没有单列的终态 (字面量取自冻结的 14 态状态机)
39
+ 'TASK_REJECTED', 'TASK_FAILED', 'TASK_CANCELLED',
40
+ // 传输层 (§1.3「新增错误码不 bump 版本」): 帧没送到 / 没有可用传输
41
+ 'TRANSPORT_UNAVAILABLE', 'TRANSPORT_FAILED',
42
+ // 通用失败码
43
+ 'C_NOT_IMPLEMENTED', 'INVALID_ARGUMENT', 'NOT_FOUND', 'TIMEOUT', 'INTERNAL_ERROR',
44
+ ];
45
+ /** §2.3 受控词表。`verify_result` 出现在 §3 表 (TASK_COMPLETED 行) 但 §2.3 表漏列 —— 以 §3 为准。 */
46
+ export const NEXT_ACTIONS = [
47
+ 'reconcile', 'approve_payment', 'needs_human', 'retry_same_request',
48
+ 'rejoin_network', 'redefine_capability', 'raise_budget', 'upgrade_client',
49
+ 'wait', 'verify_result',
50
+ ];
51
+ /** 带参数的词表项 (字面量在 payment-recovery.ts:299-300) */
52
+ export const NEXT_ACTION_PARAM_PREFIXES = ['x402_payment_retry:', 'x402_continue:'];
53
+ /** 带值的选项 (解析时要知道下一个 token 是不是值) */
54
+ const OPTIONS_WITH_VALUE = new Set([
55
+ '--request-id', '--timeout', '--budget', '--per-purchase', '--daily', '--input',
56
+ '--capability', '--name', '--price', '--per', '--description', '--wallet', '--endpoint',
57
+ '--link', '--url', '--instruction', '--provider', '--amount', '--currency', '--network',
58
+ '--per-tx', '--allow-recipient', '--allow-service', '--rate-limit', '--reason',
59
+ // 2026-09-21 (P3 收尾): bolloon-task/1 真收发 + wallet sign 用到的选项
60
+ '--payload', '--message', '--mode', '--task-id', '--deadline', '--salt', '--reply-to',
61
+ '--peer', '--via', '--eta', '--deliver',
62
+ ]);
63
+ export function parseFlags(args) {
64
+ const flags = { json: false, quiet: false, positionals: [], options: new Map(), raw: args };
65
+ for (let i = 0; i < args.length; i++) {
66
+ const a = args[i];
67
+ if (a === '--json') {
68
+ flags.json = true;
69
+ continue;
70
+ }
71
+ if (a === '--quiet' || a === '-q') {
72
+ flags.quiet = true;
73
+ continue;
74
+ }
75
+ if (a.startsWith('--')) {
76
+ const eq = a.indexOf('=');
77
+ const key = eq > 0 ? a.slice(0, eq) : a;
78
+ let value = eq > 0 ? a.slice(eq + 1) : undefined;
79
+ if (value === undefined && OPTIONS_WITH_VALUE.has(key) && i + 1 < args.length && !args[i + 1].startsWith('--')) {
80
+ value = args[++i];
81
+ }
82
+ const list = flags.options.get(key) || [];
83
+ list.push(value === undefined ? 'true' : value);
84
+ flags.options.set(key, list);
85
+ continue;
86
+ }
87
+ flags.positionals.push(a);
88
+ }
89
+ const rid = flags.options.get('--request-id')?.[0];
90
+ if (rid && rid !== 'true')
91
+ flags.requestId = rid;
92
+ const t = flags.options.get('--timeout')?.[0];
93
+ if (t && t !== 'true') {
94
+ const n = Number(t);
95
+ if (Number.isFinite(n) && n > 0)
96
+ flags.timeoutMs = Math.floor(n);
97
+ }
98
+ return flags;
99
+ }
100
+ /** 取单个选项值 (取最后一次出现的) */
101
+ export function opt(flags, name) {
102
+ const v = flags.options.get(name)?.slice(-1)[0];
103
+ return v === 'true' ? undefined : v;
104
+ }
105
+ /** 取可重复选项的全部值 */
106
+ export function optAll(flags, name) {
107
+ return (flags.options.get(name) || []).filter((v) => v !== 'true');
108
+ }
109
+ export function has(flags, name) {
110
+ return flags.options.has(name);
111
+ }
112
+ export function okEnvelope(code, message, data = {}, evidence = [], next_action = null) {
113
+ return { ok: true, code, message, data, evidence, next_action };
114
+ }
115
+ export function failEnvelope(code, message, data = {}, evidence = [], next_action = null) {
116
+ return { ok: false, code, message, data, evidence, next_action };
117
+ }
118
+ /**
119
+ * 按"这个码是不是成功码"决定 `ok` (P1 §2.1: `ok` 是唯一分派位; §3 表把成功码与失败码分开)。
120
+ * 状态类命令用它: 例如 `policy_denied` 必须是 `ok:false` —— 不许让客户端把被拒当成完成。
121
+ */
122
+ export function envelopeForState(ok, code, message, data = {}, evidence = [], next_action = null) {
123
+ return ok ? okEnvelope(code, message, data, evidence, next_action) : failEnvelope(code, message, data, evidence, next_action);
124
+ }
125
+ /** `C_NOT_IMPLEMENTED` 的统一说法: 如实说没实现 + 指出现成可用的替代路径 (绝不假装成功) */
126
+ export function notImplemented(message, data = {}) {
127
+ return failEnvelope('C_NOT_IMPLEMENTED', message, { reason: message, ...data }, [], 'upgrade_client');
128
+ }
129
+ /**
130
+ * 私钥类字段兜底剥离 (P1 §5.5): 复用契约层的 `AUDIT_FORBIDDEN_KEYS`
131
+ * (`privateKey`/`secret`/`instruction`/`taskText`/`mnemonic`/`seed`) —— 绝不让它们进 stdout。
132
+ * 命中即替换成 `[redacted]`, 并**不静默**: 顶层补 `redacted_fields` 供审计。
133
+ */
134
+ export function redactSecrets(value, hits = [], path = '$') {
135
+ if (Array.isArray(value))
136
+ return value.map((v, i) => redactSecrets(v, hits, `${path}[${i}]`));
137
+ if (value && typeof value === 'object') {
138
+ const out = {};
139
+ for (const [k, v] of Object.entries(value)) {
140
+ if (AUDIT_FORBIDDEN_KEYS.includes(k)) {
141
+ hits.push(`${path}.${k}`);
142
+ out[k] = '[redacted]';
143
+ continue;
144
+ }
145
+ out[k] = redactSecrets(v, hits, `${path}.${k}`);
146
+ }
147
+ return out;
148
+ }
149
+ return value;
150
+ }
151
+ /** 收敛成冻结字段顺序 + 兜底剥离 (所有对外输出都必须过这里) */
152
+ export function finalizeEnvelope(env, flags) {
153
+ const hits = [];
154
+ const data = redactSecrets(env.data || {}, hits);
155
+ if (hits.length)
156
+ data.redacted_fields = hits;
157
+ // `--request-id`: 幂等键原样透传/回显 (真实用它的命令会把它写进自己的查询路径)
158
+ if (flags?.requestId && data.requestId === undefined)
159
+ data.requestId = flags.requestId;
160
+ return {
161
+ ok: env.ok,
162
+ code: env.code,
163
+ message: env.message,
164
+ data,
165
+ evidence: Array.isArray(env.evidence) ? env.evidence : [],
166
+ next_action: env.next_action ?? null,
167
+ };
168
+ }
169
+ /** 老命令 (task/trace/p2p/x402) 的 `--json`: **保留既有顶层键** (基线兼容), 追加 §2 的 code/message/evidence/next_action */
170
+ export function legacyJson(payload, extra) {
171
+ const hits = [];
172
+ const redacted = redactSecrets({ ...payload, code: extra.code, message: extra.message, evidence: extra.evidence || [], next_action: extra.next_action ?? null }, hits);
173
+ if (hits.length)
174
+ redacted.redacted_fields = hits;
175
+ return redacted;
176
+ }
177
+ // ── 输出 ────────────────────────────────────────────────────────────────────
178
+ const RESET = '\x1b[0m';
179
+ const BOLD = '\x1b[1m';
180
+ // 与 cli-entry.ts 同一套 truecolor (#ef4444 / #c4d640) —— 注意必须写成十进制 RGB, 不能写 0x 字面量
181
+ const RED = '\x1b[38;2;239;68;68m';
182
+ const CYAN = '\x1b[38;2;196;214;64m';
183
+ /**
184
+ * 打印命令结果并给出退出码 (0/1)。
185
+ * · `--json` → 完整信封 (2 空格缩进, 字段顺序与 §2 一致)
186
+ * · `--quiet` → 只出结果 (payload 的 JSON)
187
+ * · 默认 → 人类可读 (命令自带); 失败时补一行 机器可读的 code/next_action, 方便不解析 JSON 也能分派
188
+ */
189
+ export function emit(env, flags, human = '') {
190
+ const e = finalizeEnvelope(env, flags);
191
+ if (flags.json) {
192
+ console.log(JSON.stringify(e, null, 2));
193
+ }
194
+ else if (flags.quiet) {
195
+ console.log(JSON.stringify(e.data));
196
+ }
197
+ else {
198
+ if (human.trim())
199
+ console.log(human.trimEnd());
200
+ if (!e.ok)
201
+ console.log(`\n${RED}✗ ${e.message}${RESET}\n code=${e.code} next_action=${e.next_action ?? 'null'}`);
202
+ }
203
+ return e.ok ? 0 : 1;
204
+ }
205
+ /** 异常兜底: 任何没被命令自己处理的错都变成结构化失败 (§2 硬规则 1) */
206
+ export function errorEnvelope(err) {
207
+ const msg = String(err?.message || err).slice(0, 300);
208
+ return failEnvelope('INTERNAL_ERROR', `命令内部异常: ${msg}`, { error: msg }, [], 'needs_human');
209
+ }
210
+ /**
211
+ * 跑一条命令: 统一套 `--timeout` (§ 超时也是结构化失败) 与异常兜底, 但**不打印**。
212
+ * 2026-09-21 (P4): 从 `runCommand` 抽出 —— MCP 适配层要的是信封本身 (它不能往 stdout 打字,
213
+ * stdout 是 JSON-RPC 流), 而超时/异常兜底必须与 CLI 是同一条实现 (绝不允许两套语义)。
214
+ */
215
+ export async function commandResult(flags, fn) {
216
+ try {
217
+ if (flags.timeoutMs)
218
+ return await raceTimeout(fn(flags), flags.timeoutMs);
219
+ return await fn(flags);
220
+ }
221
+ catch (err) {
222
+ if (err instanceof CommandTimeout) {
223
+ return {
224
+ envelope: failEnvelope('TIMEOUT', `命令超时 (--timeout ${flags.timeoutMs}ms): 已经处理的部分结果没有落盘保证`, { timeoutMs: flags.timeoutMs ?? null }, [], 'retry_same_request'),
225
+ human: '',
226
+ };
227
+ }
228
+ return { envelope: errorEnvelope(err), human: '' };
229
+ }
230
+ }
231
+ /**
232
+ * 跑一条命令: 统一套 `--timeout` (§ 超时也是结构化失败) 与异常兜底, 然后按 `--json`/`--quiet`/默认输出。
233
+ * 返回进程退出码 (0 = ok, 1 = 失败; 但**判据永远是 `ok`/`code`, 不是退出码**)。
234
+ */
235
+ export async function runCommand(flags, fn) {
236
+ const result = await commandResult(flags, fn);
237
+ return emit(result.envelope, flags, result.human);
238
+ }
239
+ class CommandTimeout extends Error {
240
+ constructor() { super('command timeout'); this.name = 'CommandTimeout'; }
241
+ }
242
+ function raceTimeout(work, ms) {
243
+ return new Promise((resolve, reject) => {
244
+ let settled = false;
245
+ const timer = setTimeout(() => { if (!settled) {
246
+ settled = true;
247
+ reject(new CommandTimeout());
248
+ } }, ms);
249
+ work.then((v) => { if (!settled) {
250
+ settled = true;
251
+ clearTimeout(timer);
252
+ resolve(v);
253
+ } })
254
+ .catch((e) => { if (!settled) {
255
+ settled = true;
256
+ clearTimeout(timer);
257
+ reject(e);
258
+ } });
259
+ });
260
+ }
261
+ /** 人类可读输出的小工具 (与 cli-entry.ts 的配色一致) */
262
+ export const tone = { reset: RESET, bold: BOLD, red: RED, cyan: CYAN };
263
+ /** 把带 ANSI 色的帮助文本变成纯文本 (放进 `--json` 的 data 里时必须先过它) */
264
+ export function plain(t) {
265
+ // eslint-disable-next-line no-control-regex
266
+ return String(t).replace(/\u001b\[[0-9;]*m/g, '');
267
+ }
268
+ export function line(label, value) {
269
+ return ` ${String(label).padEnd(14)} ${value === undefined || value === null || value === '' ? '(无)' : String(value)}`;
270
+ }
271
+ export function title(t) {
272
+ return `\n${BOLD}${t}${RESET}`;
273
+ }
274
+ export function hint(t) {
275
+ return `${CYAN}${t}${RESET}`;
276
+ }
277
+ /** 一行内看结论的失败摘要 (人读模式常用) */
278
+ export function failHuman(head, env) {
279
+ return `${head}\n 原因: ${env.message}\n code: ${env.code} · 下一步: ${env.next_action ?? '无需额外动作'}`;
280
+ }
281
+ /** 任务 14 态 (§4.1) → 对外 4 态 */
282
+ export function publicStatusForTask(state, nextAction) {
283
+ switch (state) {
284
+ case 'discovered':
285
+ case 'quoted':
286
+ return '准备中';
287
+ case 'submitted':
288
+ case 'accepted':
289
+ case 'paid':
290
+ return '正在获取能力';
291
+ case 'paying':
292
+ return nextAction === 'reconcile' ? '需要你处理' : '正在获取能力';
293
+ case 'payment_required':
294
+ case 'policy_denied':
295
+ case 'rejected':
296
+ case 'failed':
297
+ case 'cancelled':
298
+ return '需要你处理';
299
+ case 'running':
300
+ return '正在执行';
301
+ case 'delivered':
302
+ return '已完成或需要你处理';
303
+ case 'verified':
304
+ return '已完成';
305
+ default:
306
+ return '需要你处理';
307
+ }
308
+ }
309
+ /** 交易 11 态 (§4.2) → 对外 4 态 */
310
+ export function publicStatusForTrade(status, nextAction) {
311
+ switch (status) {
312
+ case 'discovered':
313
+ case 'quoted':
314
+ return '准备中';
315
+ case 'paying':
316
+ case 'settled':
317
+ return nextAction === 'reconcile' ? '需要你处理' : '正在获取能力';
318
+ case 'delivered':
319
+ return '已完成或需要你处理';
320
+ case 'verified':
321
+ return '已完成';
322
+ case 'payment_required':
323
+ case 'policy_denied':
324
+ case 'delivery_failed':
325
+ case 'verification_failed':
326
+ case 'disputed':
327
+ case 'failed':
328
+ return '需要你处理';
329
+ default:
330
+ return '需要你处理';
331
+ }
332
+ }
333
+ /**
334
+ * 交易记录的对外口径 —— **local-dev 一律标 `local-dev`, 绝不冒充链上** (P1 §5.1 第 3 条)。
335
+ * `success` 用 §7.1 的判据: `status==='verified'` ∧ 事实 ∈ {fully_settled, payment_verified}。
336
+ */
337
+ export function settlementLabel(rec) {
338
+ if (rec.paymentMode === 'local-dev')
339
+ return 'local-dev';
340
+ if (rec.chainSettled === true && ['payment_verified', 'partially_settled', 'fully_settled'].includes(String(rec.settlementFact || '')))
341
+ return 'chain';
342
+ return 'none';
343
+ }
344
+ export function isTradeSuccessful(rec) {
345
+ return String(rec.status) === 'verified' && ['fully_settled', 'payment_verified'].includes(String(rec.settlementFact || ''));
346
+ }
package/dist/cli-entry.js CHANGED
@@ -22,8 +22,15 @@ import { discoverEngines, delegateToEngine } from './external-engines/index.js';
22
22
  import { x402CheckBalance, x402Fetch } from './agents/x402/x402Pay.js';
23
23
  import { runVersionCommand, runUpdateCommand, runDoctorCommand, runRuntimeCommand, UPDATE_HELP } from './cli/update-commands.js';
24
24
  import { collectVersionInfo } from './utils/version-info.js';
25
+ // 2026-09-21 (P3): 统一 JSON 信封 + 命令组 (network/agent/task/wallet/payment/trade)
26
+ import { runServiceGroup, GROUPS_HELP } from './cli/commands/index.js';
27
+ // 2026-09-21 (P4): MCP 适配层 (`bolloon mcp serve` = stdio, 只调 P3 服务层)
28
+ import { runMcpCommand } from './cli/commands/mcp.js';
29
+ import { legacyJson } from './cli/protocol-envelope.js';
25
30
  import { createRequire } from 'module';
26
31
  const _require = createRequire(import.meta.url);
32
+ /** P3 `bolloon task <子命令>` 的已知子命令 —— 其它一律当 M1 任务正文 (既有体验不动) */
33
+ const TASK_SUBCOMMANDS = new Set(['send', 'list', 'status', 'cancel', 'retry', 'result', 'inbox', 'accept', 'reject', 'run', 'complete']);
27
34
  const isWindows = process.platform === 'win32';
28
35
  // ANSI 颜色 — Bolloon Web UI 配色 truecolor
29
36
  function _fg(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
@@ -68,6 +75,10 @@ ${BOLD}命令:${RESET}
68
75
  bolloon engine run <prompt> 委派任务给智能体
69
76
  bolloon x402 fetch <url> x402 自动支付 HTTP 请求
70
77
  bolloon x402 balance <address> 查询 x402 钱包余额
78
+ bolloon mcp serve MCP server (stdio) —— 给 MCP 客户端接本机 Agent 能力
79
+ bolloon mcp tools 列出 MCP 暴露的 tools/resources (只读清单)
80
+
81
+ ${GROUPS_HELP}
71
82
 
72
83
  ${BOLD}示例:${RESET}
73
84
  bolloon # 启动图形界面
@@ -78,6 +89,8 @@ ${BOLD}示例:${RESET}
78
89
  bolloon update plan # 看更新计划与风险检查
79
90
  bolloon update now # 真正执行更新
80
91
  bolloon doctor # 我这台机器的 Bolloon 是否自洽
92
+ bolloon network join --json # 入网 (失败也结构化: ok:false + code + next_action)
93
+ bolloon task --request-id <id> "…" # 用给定幂等键跑 M1 任务 (同一 requestId 不会付两次)
81
94
  ${UPDATE_HELP}
82
95
 
83
96
  ${BOLD}环境变量:${RESET}
@@ -169,6 +182,20 @@ function parseArgs() {
169
182
  // 2026-09-18: M1 唯一入口 —— 一个任务 → 一个 Skill → 一个报告卡
170
183
  case 'task':
171
184
  return { mode: 'task', args: args.slice(1) };
185
+ // 2026-09-21: P3 命令组 (统一信封 + 全局选项; 薄包装现有服务)
186
+ case 'network':
187
+ return { mode: 'network', args: args.slice(1) };
188
+ case 'agent':
189
+ return { mode: 'agent', args: args.slice(1) };
190
+ case 'wallet':
191
+ return { mode: 'wallet', args: args.slice(1) };
192
+ case 'payment':
193
+ return { mode: 'payment', args: args.slice(1) };
194
+ case 'trade':
195
+ return { mode: 'trade', args: args.slice(1) };
196
+ // 2026-09-21: P4 MCP 适配层 (`bolloon mcp serve` = stdio MCP server; 只调 P3 服务层)
197
+ case 'mcp':
198
+ return { mode: 'mcp', args: args.slice(1) };
172
199
  // 2026-09-13: 初始化向导 (用户身份 + 模型供应商 + API key)
173
200
  case 'setup':
174
201
  case 'init':
@@ -250,7 +277,18 @@ async function handleX402Command(x402Args) {
250
277
  rpcUrl: readOption(rest, '--rpc-url'),
251
278
  });
252
279
  if (hasFlag(rest, '--json')) {
253
- console.log(JSON.stringify(result, null, 2));
280
+ // 2026-09-21 (P3): 保留既有 {success,data,status,error,paymentInfo,...}, 追加 code/message/next_action
281
+ console.log(JSON.stringify(legacyJson(result, {
282
+ code: result.success ? 'OK' : 'INTERNAL_ERROR',
283
+ message: result.success ? `x402 请求完成 (status=${result.status})` : `x402 请求失败: ${result.error || ''}`,
284
+ evidence: [],
285
+ next_action: result.success ? null : 'retry_same_request',
286
+ }), null, 2));
287
+ if (!result.success)
288
+ process.exit(1);
289
+ }
290
+ else if (hasFlag(rest, '--quiet')) {
291
+ console.log(JSON.stringify(result));
254
292
  if (!result.success)
255
293
  process.exit(1);
256
294
  }
@@ -280,7 +318,17 @@ async function handleX402Command(x402Args) {
280
318
  rpcUrl: readOption(rest, '--rpc-url'),
281
319
  });
282
320
  if (hasFlag(rest, '--json')) {
283
- console.log(JSON.stringify(result, null, 2));
321
+ console.log(JSON.stringify(legacyJson(result, {
322
+ code: result.success ? 'OK' : 'INTERNAL_ERROR',
323
+ message: result.success ? `余额查询完成 (${result.network})` : `余额查询失败: ${result.error || ''}`,
324
+ evidence: [address],
325
+ next_action: result.success ? null : 'retry_same_request',
326
+ }), null, 2));
327
+ if (!result.success)
328
+ process.exit(1);
329
+ }
330
+ else if (hasFlag(rest, '--quiet')) {
331
+ console.log(JSON.stringify(result));
284
332
  if (!result.success)
285
333
  process.exit(1);
286
334
  }
@@ -317,6 +365,7 @@ async function handleX402Command(x402Args) {
317
365
  async function handleTaskCommand(taskArgs) {
318
366
  const { runTask, resumeTask } = await import('./agents/task/task-runner.js');
319
367
  const wantJson = taskArgs.includes('--json');
368
+ const wantQuiet = taskArgs.includes('--quiet');
320
369
  const flag = (name) => {
321
370
  const i = taskArgs.indexOf(name);
322
371
  return i >= 0 ? taskArgs[i + 1] : undefined;
@@ -326,6 +375,8 @@ async function handleTaskCommand(taskArgs) {
326
375
  const perPurchase = flag('--per-purchase');
327
376
  const daily = flag('--daily');
328
377
  const inputRaw = flag('--input');
378
+ // 2026-09-21 (P3): 幂等键透传 —— 同一 requestId 重发不会产生第二笔付款 (契约层派生逻辑不变)
379
+ const requestId = flag('--request-id');
329
380
  let input;
330
381
  if (inputRaw !== undefined) {
331
382
  try {
@@ -338,14 +389,24 @@ async function handleTaskCommand(taskArgs) {
338
389
  }
339
390
  const STAGE_LABEL = { prepare: '准备中', acquire: '正在获取能力', execute: '正在执行', report: '报告' };
340
391
  const onStage = (stage, note) => {
341
- if (wantJson)
392
+ if (wantJson || wantQuiet)
342
393
  return;
343
394
  console.error(` ${CYAN}${STAGE_LABEL[stage] || stage}${RESET} ${note}`);
344
395
  };
345
396
  if (resumeId) {
346
397
  const r = await resumeTask({ goalId: resumeId, input, allowLocalDev: true });
347
- if (wantJson)
348
- console.log(JSON.stringify({ resumed: r.resumed, action: r.action, reason: r.reason, mustNotRepay: r.mustNotRepay, card: r.card }, null, 2));
398
+ const verdict = legacyTaskVerdict(r);
399
+ const payload = {
400
+ resumed: r.resumed, action: r.action, reason: r.reason, mustNotRepay: r.mustNotRepay, card: r.card,
401
+ goalId: r.goalId, runId: r.runId ?? null, transactionId: r.transactionId ?? null,
402
+ };
403
+ if (wantQuiet)
404
+ console.log(JSON.stringify(payload));
405
+ else if (wantJson)
406
+ console.log(JSON.stringify(legacyJson(payload, {
407
+ code: verdict.code, message: verdict.message, next_action: verdict.next,
408
+ evidence: [r.goalId, r.runId, r.transactionId].filter(Boolean),
409
+ }), null, 2));
349
410
  else {
350
411
  console.log('');
351
412
  console.log(r.text);
@@ -357,7 +418,7 @@ async function handleTaskCommand(taskArgs) {
357
418
  if (a.startsWith('--'))
358
419
  return false;
359
420
  const prev = taskArgs[i - 1];
360
- return !['--budget', '--per-purchase', '--daily', '--input'].includes(prev || '');
421
+ return !['--budget', '--per-purchase', '--daily', '--input', '--request-id', '--timeout'].includes(prev || '');
361
422
  });
362
423
  const task = words.join(' ').trim();
363
424
  if (!task) {
@@ -372,19 +433,27 @@ ${CYAN}bolloon task --resume <goalId>${RESET}
372
433
  --per-purchase <USDC> 单次购买上限 (M1 硬上限 0.02)
373
434
  --daily <USDC> 当日预算 (M1 硬上限 0.10)
374
435
  --input '<json>' 显式给技能输入 (跳过自动推导)
375
- --json 机器可读输出
436
+ --request-id <id> 指定幂等键 (同一 requestId 重发不会产生第二笔付款)
437
+ --json 机器可读输出 (+ code/next_action/evidence; 失败也结构化)
376
438
  `);
377
439
  return;
378
440
  }
379
- const r = await runTask({ task, budget, perPurchase, daily, input, allowLocalDev: true, onStage });
380
- if (wantJson) {
381
- console.log(JSON.stringify({
441
+ const r = await runTask({ task, budget, perPurchase, daily, input, requestId, allowLocalDev: true, onStage });
442
+ const verdict = legacyTaskVerdict(r);
443
+ if (wantQuiet) {
444
+ console.log(JSON.stringify({ ok: r.ok, goalId: r.goalId, runId: r.runId, transactionId: r.transactionId, card: r.card, payment: r.payment }));
445
+ }
446
+ else if (wantJson) {
447
+ console.log(JSON.stringify(legacyJson({
382
448
  ok: r.ok, status: r.card.status, conclusion: r.card.conclusion, card: r.card,
383
449
  goalId: r.goalId, runId: r.runId, transactionId: r.transactionId,
384
450
  advisor: r.advisor, payment: r.payment, outputIssues: r.outputIssues,
385
451
  budget: { taskBudget: r.budget.taskBudget, perPurchase: r.budget.perPurchase, daily: r.budget.daily, clamped: r.budget.clamped },
386
452
  stages: r.stages,
387
- }, null, 2));
453
+ }, {
454
+ code: verdict.code, message: verdict.message, next_action: verdict.next,
455
+ evidence: [r.goalId, r.runId, r.transactionId].filter(Boolean),
456
+ }), null, 2));
388
457
  }
389
458
  else {
390
459
  console.log('');
@@ -394,17 +463,39 @@ ${CYAN}bolloon task --resume <goalId>${RESET}
394
463
  // 一次性命令: 显式收尾 (DIAP/HTTP 句柄不该吊住进程)
395
464
  process.exit(r.ok ? 0 : 1);
396
465
  }
466
+ /**
467
+ * 老 M1 命令 (`bolloon task`) 的 §3 码映射 (P1 §3 + §5.1 红线):
468
+ * local-dev 最高到 `delivered` → 一律 `TASK_COMPLETED` + `verify_result`, **绝不** TASK_VERIFIED。
469
+ */
470
+ function legacyTaskVerdict(r) {
471
+ const chain = r.payment?.chainSettled === true || r.card?.payment?.chainSettled === true;
472
+ if (r.ok) {
473
+ return chain
474
+ ? { code: 'TASK_VERIFIED', message: 'Task verified', next: null }
475
+ : { code: 'TASK_COMPLETED', message: 'Task delivered (local-dev: 链上未结算, 不算成功)', next: 'verify_result' };
476
+ }
477
+ return r.card?.hardGate === 'bought_not_executed'
478
+ ? { code: 'DELIVERY_FAILED', message: '已付款但没有交付 → 绝不重付, 交人处理', next: 'needs_human' }
479
+ : { code: 'RESULT_UNVERIFIED', message: '任务没走完/未过验真门', next: 'needs_human' };
480
+ }
397
481
  async function handleTraceCommand(traceArgs) {
398
482
  const { listRuns, readRun } = await import('./agents/run-store.js');
399
483
  const { runToTraceText, runToTraceJson, summarizeTrace } = await import('./agents/trace-export.js');
400
484
  const wantJson = traceArgs.includes('--json');
485
+ const wantQuiet = traceArgs.includes('--quiet');
401
486
  const lastIdx = traceArgs.indexOf('--last');
402
487
  const last = lastIdx >= 0 ? Number(traceArgs[lastIdx + 1]) : undefined;
403
488
  const runId = traceArgs.find((a) => !a.startsWith('--') && a !== String(last));
404
489
  if (!runId) {
405
490
  const runs = await listRuns({ limit: 20 });
491
+ const payloads = runs.map((r) => runToTraceJson(r));
492
+ if (wantQuiet) {
493
+ console.log(JSON.stringify(payloads));
494
+ return;
495
+ }
406
496
  if (wantJson) {
407
- console.log(JSON.stringify(runs.map((r) => runToTraceJson(r)), null, 2));
497
+ // 2026-09-21 (P3): 列表形式也走统一信封 (此前是裸数组; `data` 里仍是同样的 payload 数组)
498
+ console.log(JSON.stringify({ ok: true, code: 'OK', message: `最近 ${runs.length} 次运行`, data: payloads, evidence: payloads.map((p) => p.runId).filter(Boolean), next_action: null }, null, 2));
408
499
  return;
409
500
  }
410
501
  console.log(`\n${BOLD}最近 ${runs.length} 次运行的执行轨迹${RESET}\n`);
@@ -425,12 +516,19 @@ async function handleTraceCommand(traceArgs) {
425
516
  const run = await readRun(runId);
426
517
  if (!run) {
427
518
  console.error(`${MAGENTA}没有这个运行: ${runId}${RESET}`);
519
+ if (wantJson)
520
+ console.log(JSON.stringify({ ok: false, code: 'NOT_FOUND', message: `没有这个运行: ${runId}`, data: { runId }, evidence: [], next_action: 'retry_same_request' }, null, 2));
428
521
  process.exitCode = 1;
429
522
  return;
430
523
  }
431
- console.log(wantJson ? JSON.stringify(runToTraceJson(run), null, 2) : runToTraceText(run, { limit: last }));
432
- if (!wantJson) {
433
- const j = runToTraceJson(run);
524
+ const j = runToTraceJson(run);
525
+ if (wantQuiet)
526
+ console.log(JSON.stringify(j));
527
+ else if (wantJson)
528
+ console.log(JSON.stringify(legacyJson(j, { code: 'OK', message: `运行 ${runId} 轨迹`, evidence: [runId], next_action: null }), null, 2));
529
+ else
530
+ console.log(runToTraceText(run, { limit: last }));
531
+ if (!wantJson && !wantQuiet) {
434
532
  console.error(`\n${CYAN}(${j.counts.total} 步 · ✓${j.counts.ok} / ✗${j.counts.fail} · ${j.counts.totalMs}ms · 状态 ${j.status})${RESET}`);
435
533
  }
436
534
  }
@@ -442,8 +540,19 @@ async function handleTraceCommand(traceArgs) {
442
540
  async function handleP2pCommand(p2pArgs) {
443
541
  const { getLocalP2pInfo, formatP2pInfoText, formatP2pInfoJson } = await import('./agents/p2p-info.js');
444
542
  const info = await getLocalP2pInfo();
445
- if (p2pArgs.includes('--json'))
446
- console.log(formatP2pInfoJson(info));
543
+ if (p2pArgs.includes('--quiet')) {
544
+ console.log(JSON.stringify(JSON.parse(formatP2pInfoJson(info))));
545
+ }
546
+ else if (p2pArgs.includes('--json')) {
547
+ // 2026-09-21 (P3): 保留 bolloon-p2p-info/1 的全部既有字段, 追加 §2 的 code/message/evidence/next_action
548
+ const payload = JSON.parse(formatP2pInfoJson(info));
549
+ console.log(JSON.stringify(legacyJson(payload, {
550
+ code: 'OK',
551
+ message: info.ok ? `本机 P2P 信息 (peerId=${String(info.peerId).slice(0, 16)}…)` : '还没有本机 peerId',
552
+ evidence: info.peerId ? [String(info.peerId)] : [],
553
+ next_action: info.ok ? null : 'rejoin_network',
554
+ }), null, 2));
555
+ }
447
556
  else
448
557
  console.log(formatP2pInfoText(info));
449
558
  // 一次性信息命令必须自己收尾: 读运行中节点会 import network/p2p (libp2p),
@@ -755,8 +864,23 @@ async function main() {
755
864
  break;
756
865
  // 2026-09-18: M1 任务闭环 (bolloon task "<任务>" --budget 0.05 / --resume <goalId>)
757
866
  case 'task':
867
+ // 2026-09-21 (P3): 已知子命令 → 命令组 (统一信封); 其它一律当 M1 任务正文 (既有体验不动)
868
+ if (args[0] && TASK_SUBCOMMANDS.has(args[0]))
869
+ process.exit(await runServiceGroup('task', args));
758
870
  await handleTaskCommand(args);
759
871
  break;
872
+ // 2026-09-21 (P3): 命令组 (network/agent/wallet/payment/trade) —— 统一信封 + 薄包装现有服务
873
+ case 'network':
874
+ case 'agent':
875
+ case 'wallet':
876
+ case 'payment':
877
+ case 'trade':
878
+ process.exit(await runServiceGroup(mode, args));
879
+ break;
880
+ // 2026-09-21 (P4): MCP 适配层 (`bolloon mcp serve` = stdio MCP server; tools/resources 只调 P3 服务层)
881
+ case 'mcp':
882
+ process.exit(await runMcpCommand(args));
883
+ break;
760
884
  // 2026-09-13: bolloon setup — 首次运行初始化向导
761
885
  case 'setup':
762
886
  await handleSetupCommand(args);
@@ -151,6 +151,11 @@ async function gatewayCallAgent(opts) {
151
151
  return { success: false, error: r.error, provider: provider.agentId, decision: 'allow' };
152
152
  // 7. Reputation: 记录成功
153
153
  await (0, agent_reputation_js_1.recordServiceOutcome)(provider.agentId, provider.service?.name || 'service', 'success', registry).catch(() => { });
154
+ // 2026-09-18: 委派完成 → 网络脉冲 (只记"发生过一次成功委派", 不记任务内容/对端地址; 失败静默)
155
+ try {
156
+ void Promise.resolve().then(() => __importStar(require('./network-pulse.js'))).then((np) => np.recordNetworkEvent({ type: 'delegation_completed', did: String(opts.toPublicKey || provider.agentId || 'unknown-node') })).catch(() => { });
157
+ }
158
+ catch { /* ignore */ }
154
159
  return { success: true, provider: provider.agentId, decision: 'allow', output: r.output };
155
160
  }
156
161
  catch (e) {