agent2agent-cli 0.2.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.
Files changed (2) hide show
  1. package/a2a.js +1095 -0
  2. package/package.json +34 -0
package/a2a.js ADDED
@@ -0,0 +1,1095 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent2Agent 统一 CLI —— `a2a`
4
+ *
5
+ * 零第三方依赖:仅使用 Node 内置模块(fs / path / crypto)。
6
+ * HTTP 使用 Node >= 20 的全局 fetch;multipart 上传使用全局 FormData / Blob。
7
+ *
8
+ * 用法:
9
+ * node cli/a2a.js <命令> [选项]
10
+ * (或 chmod +x 后直接 ./cli/a2a.js)
11
+ *
12
+ * 配置:项目根目录 .a2a.json(从当前目录逐级向上查找,或 --config 指定)
13
+ * 状态:.a2a-state.json(与配置同目录,记录同步游标与本地 manifest)
14
+ */
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const crypto = require('crypto');
20
+
21
+ /* ------------------------------------------------------------------------- *
22
+ * 颜色 / 排版工具(纯文本可用,终端下自动着色,NO_COLOR 可关闭)
23
+ * ------------------------------------------------------------------------- */
24
+
25
+ const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
26
+ const C = {
27
+ reset: useColor ? '\x1b[0m' : '',
28
+ bold: useColor ? '\x1b[1m' : '',
29
+ dim: useColor ? '\x1b[2m' : '',
30
+ red: useColor ? '\x1b[31m' : '',
31
+ green: useColor ? '\x1b[32m' : '',
32
+ yellow: useColor ? '\x1b[33m' : '',
33
+ blue: useColor ? '\x1b[34m' : '',
34
+ cyan: useColor ? '\x1b[36m' : '',
35
+ };
36
+ const paint = (color, s) => `${color}${s}${C.reset}`;
37
+ const hl = (s) => paint(C.bold + C.cyan, s); // 命令名高亮
38
+
39
+ /* 字符显示宽度(CJK 按 2 列) */
40
+ function charWidth(ch) {
41
+ const code = ch.codePointAt(0);
42
+ if (
43
+ (code >= 0x1100 && code <= 0x115f) ||
44
+ (code >= 0x2e80 && code <= 0xa4cf) ||
45
+ (code >= 0xac00 && code <= 0xd7a3) ||
46
+ (code >= 0xf900 && code <= 0xfaff) ||
47
+ (code >= 0xfe30 && code <= 0xfe4f) ||
48
+ (code >= 0xff00 && code <= 0xff60) ||
49
+ (code >= 0xffe0 && code <= 0xffe6) ||
50
+ (code >= 0x1f300 && code <= 0x1faff)
51
+ ) {
52
+ return 2;
53
+ }
54
+ return 1;
55
+ }
56
+ function displayWidth(s) {
57
+ return Array.from(String(s)).reduce((w, c) => w + charWidth(c), 0);
58
+ }
59
+ function padRight(s, w) {
60
+ const d = w - displayWidth(s);
61
+ return s + ' '.repeat(Math.max(0, d));
62
+ }
63
+ function padLeft(s, w) {
64
+ const d = w - displayWidth(s);
65
+ return ' '.repeat(Math.max(0, d)) + s;
66
+ }
67
+
68
+ function fmtTime(ms) {
69
+ if (!ms && ms !== 0) return '-';
70
+ const d = new Date(ms);
71
+ const p2 = (n) => String(n).padStart(2, '0');
72
+ return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}`;
73
+ }
74
+ function fmtAgo(ms) {
75
+ if (!ms && ms !== 0) return '从未';
76
+ const diff = Date.now() - ms;
77
+ if (diff < 60 * 1000) return '刚刚';
78
+ if (diff < 3600 * 1000) return `${Math.floor(diff / 60000)}分钟前`;
79
+ if (diff < 24 * 3600 * 1000) return `${Math.floor(diff / 3600000)}小时前`;
80
+ return `${Math.floor(diff / 86400000)}天前`;
81
+ }
82
+ function fmtSize(n) {
83
+ if (n == null) return '-';
84
+ if (n < 1024) return `${n}B`;
85
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
86
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
87
+ }
88
+ function fmtTaskStats(stats) {
89
+ if (!stats) return '-';
90
+ const t = stats.todo ?? 0;
91
+ const d = stats.doing ?? 0;
92
+ const b = stats.blocked ?? 0;
93
+ const dn = stats.done ?? 0;
94
+ return `待${t}/做${d}/阻${b}/完${dn}`;
95
+ }
96
+
97
+ /* 通用表格渲染:headers 为列名数组,rows 为字符串数组的数组 */
98
+ function renderTable(headers, rows) {
99
+ const widths = headers.map((h, i) => {
100
+ let w = displayWidth(h);
101
+ for (const r of rows) w = Math.max(w, displayWidth(String(r[i] ?? '-')));
102
+ return w;
103
+ });
104
+ const line = (cells) => cells.map((c, i) => padRight(String(c ?? '-'), widths[i])).join(' ');
105
+ const sep = widths.map((w) => '-'.repeat(w)).join(' ');
106
+ const out = [line(headers), sep];
107
+ for (const r of rows) out.push(line(r));
108
+ return out.join('\n');
109
+ }
110
+
111
+ /* ------------------------------------------------------------------------- *
112
+ * 错误与退出
113
+ * ------------------------------------------------------------------------- */
114
+
115
+ class ApiError extends Error {
116
+ constructor(message, status, bodyText) {
117
+ super(message);
118
+ this.status = status;
119
+ this.bodyText = bodyText;
120
+ }
121
+ }
122
+
123
+ /** 打印错误到 stderr 并以非零码退出 */
124
+ function fail(msg) {
125
+ process.stderr.write(paint(C.red, '错误: ') + String(msg) + '\n');
126
+ process.exit(1);
127
+ }
128
+
129
+ /** 从服务端错误正文里解析出 message(统一错误格式 {error:{message}}) */
130
+ function parseErrorMessage(text, status) {
131
+ try {
132
+ const j = JSON.parse(text);
133
+ if (j && j.error && typeof j.error === 'object') {
134
+ if (j.error.message) {
135
+ return `${j.error.message}${j.error.code != null ? ` (code ${j.error.code})` : ''}`;
136
+ }
137
+ return JSON.stringify(j.error);
138
+ }
139
+ if (j && typeof j.error === 'string') return j.error;
140
+ if (j && j.message) return j.message;
141
+ } catch {
142
+ /* 非 JSON 正文,走下方兜底 */
143
+ }
144
+ const firstLine = (text || '').split('\n')[0].trim();
145
+ if (firstLine) return firstLine.slice(0, 300);
146
+ return `HTTP ${status}`;
147
+ }
148
+
149
+ /* ------------------------------------------------------------------------- *
150
+ * 参数解析(零依赖)
151
+ *
152
+ * --flag → true(布尔)
153
+ * --key value → 'value'
154
+ * --key=value → 'value'
155
+ * 重复的 --key → 累积为数组
156
+ * -- 之后全部为位置参数
157
+ * ------------------------------------------------------------------------- */
158
+
159
+ function parseArgs(argv) {
160
+ const pos = [];
161
+ const opts = {};
162
+ let i = 0;
163
+ while (i < argv.length) {
164
+ const a = argv[i];
165
+ if (a === '--') {
166
+ pos.push(...argv.slice(i + 1));
167
+ break;
168
+ }
169
+ if (a.startsWith('--')) {
170
+ const eq = a.indexOf('=');
171
+ let key;
172
+ let val;
173
+ if (eq >= 0) {
174
+ key = a.slice(2, eq);
175
+ val = a.slice(eq + 1);
176
+ } else {
177
+ key = a.slice(2);
178
+ if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
179
+ val = argv[i + 1];
180
+ i++;
181
+ } else {
182
+ val = true;
183
+ }
184
+ }
185
+ if (key in opts) {
186
+ if (Array.isArray(opts[key])) opts[key].push(val);
187
+ else opts[key] = [opts[key], val];
188
+ } else {
189
+ opts[key] = val;
190
+ }
191
+ } else {
192
+ pos.push(a);
193
+ }
194
+ i++;
195
+ }
196
+ return { pos, opts };
197
+ }
198
+
199
+ /** 把「单值或数组」规整为数组 */
200
+ function listOpt(v) {
201
+ if (v === undefined || v === null) return [];
202
+ return Array.isArray(v) ? v : [v];
203
+ }
204
+
205
+ /** 逗号分隔列表(--capabilities a,b / --tech x,y)规整为去空字符串数组 */
206
+ function splitList(v) {
207
+ return listOpt(v)
208
+ .flatMap((x) => String(x).split(','))
209
+ .map((s) => s.trim())
210
+ .filter(Boolean);
211
+ }
212
+
213
+ /* ------------------------------------------------------------------------- *
214
+ * 配置与状态文件
215
+ * ------------------------------------------------------------------------- */
216
+
217
+ /** 从 startDir 逐级向上查找 .a2a.json */
218
+ function findConfigFile(startDir) {
219
+ let dir = path.resolve(startDir);
220
+ for (;;) {
221
+ const candidate = path.join(dir, '.a2a.json');
222
+ if (fs.existsSync(candidate)) return candidate;
223
+ const parent = path.dirname(dir);
224
+ if (parent === dir) return null;
225
+ dir = parent;
226
+ }
227
+ }
228
+
229
+ /** 加载配置,返回 { config, dir, file } */
230
+ function requireConfig(configPath) {
231
+ const file = configPath ? path.resolve(process.cwd(), configPath) : findConfigFile(process.cwd());
232
+ if (!file) {
233
+ fail('未找到 .a2a.json(已从当前目录逐级向上查找)。请先运行 a2a init 注册账号。');
234
+ }
235
+ if (!fs.existsSync(file)) fail(`配置文件不存在: ${file}`);
236
+ let config;
237
+ try {
238
+ config = JSON.parse(fs.readFileSync(file, 'utf8'));
239
+ } catch (e) {
240
+ fail(`配置文件解析失败: ${file}(${e.message})`);
241
+ }
242
+ if (!config.url) fail(`配置文件缺少 url 字段: ${file}`);
243
+ if (!config.accountId) fail(`配置文件缺少 accountId 字段: ${file}`);
244
+ if (!config.token) fail(`配置文件缺少 token 字段: ${file}`);
245
+ return { config, dir: path.dirname(file), file };
246
+ }
247
+
248
+ function defaultState() {
249
+ return { lastSync: 0, lastSeq: 0, manifest: {} };
250
+ }
251
+
252
+ function stateFile(dir) {
253
+ return path.join(dir, '.a2a-state.json');
254
+ }
255
+
256
+ function loadState(dir) {
257
+ const p = stateFile(dir);
258
+ if (fs.existsSync(p)) {
259
+ try {
260
+ const s = JSON.parse(fs.readFileSync(p, 'utf8'));
261
+ return {
262
+ lastSync: s.lastSync || 0,
263
+ lastSeq: s.lastSeq || 0,
264
+ manifest: s.manifest && typeof s.manifest === 'object' ? s.manifest : {},
265
+ };
266
+ } catch {
267
+ return defaultState();
268
+ }
269
+ }
270
+ return defaultState();
271
+ }
272
+
273
+ function saveState(dir, state) {
274
+ fs.writeFileSync(stateFile(dir), JSON.stringify(state, null, 2) + '\n');
275
+ }
276
+
277
+ /** 解析 doc 目录为绝对路径(相对路径相对于配置文件所在目录) */
278
+ function resolveDocDir(config, configDir) {
279
+ const d = config.docDir || '.a2a/docs';
280
+ return path.isAbsolute(d) ? d : path.resolve(configDir, d);
281
+ }
282
+
283
+ /* ------------------------------------------------------------------------- *
284
+ * HTTP 调用
285
+ * ------------------------------------------------------------------------- */
286
+
287
+ function baseUrl(config) {
288
+ let b = String(config.url || '').replace(/\/+$/, '');
289
+ if (!/\/api\/v1$/.test(b)) b += '/api/v1';
290
+ return b;
291
+ }
292
+
293
+ /**
294
+ * 统一 API 调用。
295
+ * opts:
296
+ * query : 查询参数对象
297
+ * body : JSON 对象(自动 Content-Type: application/json)
298
+ * form : FormData(fetch 自动设置 multipart boundary)
299
+ * raw : true 时返回 { res, buf }(buf 为 Buffer,用于下载二进制)
300
+ */
301
+ async function api(config, method, pathName, opts = {}) {
302
+ const { query, body, form, raw } = opts;
303
+ let url = baseUrl(config) + pathName;
304
+ if (query) {
305
+ const qs = new URLSearchParams(query).toString();
306
+ if (qs) url += (url.includes('?') ? '&' : '?') + qs;
307
+ }
308
+
309
+ const headers = {};
310
+ if (config && config.token) headers['Authorization'] = 'Bearer ' + config.token;
311
+
312
+ let fetchBody;
313
+ if (form) {
314
+ fetchBody = form; // FormData:由 fetch 生成 multipart 头
315
+ } else if (body !== undefined) {
316
+ headers['Content-Type'] = 'application/json';
317
+ fetchBody = JSON.stringify(body);
318
+ }
319
+
320
+ let res;
321
+ try {
322
+ res = await fetch(url, { method, headers, body: fetchBody });
323
+ } catch (e) {
324
+ throw new ApiError(`网络请求失败: ${e.message}`, 0, '');
325
+ }
326
+
327
+ if (!res.ok) {
328
+ let text = '';
329
+ try {
330
+ text = await res.text();
331
+ } catch {
332
+ /* ignore */
333
+ }
334
+ throw new ApiError(parseErrorMessage(text, res.status), res.status, text);
335
+ }
336
+
337
+ if (raw) {
338
+ const buf = Buffer.from(await res.arrayBuffer());
339
+ return { res, buf };
340
+ }
341
+
342
+ const ct = res.headers.get('content-type') || '';
343
+ if (ct.includes('application/json')) {
344
+ try {
345
+ return await res.json();
346
+ } catch {
347
+ return await res.text();
348
+ }
349
+ }
350
+ return await res.text();
351
+ }
352
+
353
+ /** 把「数组 / {items:[...]} / {documents:[...]} / {tasks:[...]}」统一为数组 */
354
+ function toArray(res) {
355
+ if (Array.isArray(res)) return res;
356
+ if (res && Array.isArray(res.items)) return res.items;
357
+ if (res && Array.isArray(res.documents)) return res.documents;
358
+ if (res && Array.isArray(res.tasks)) return res.tasks;
359
+ return [];
360
+ }
361
+
362
+ /* ------------------------------------------------------------------------- *
363
+ * 双向镜像同步(§11.2 / api.md §5)
364
+ * ------------------------------------------------------------------------- */
365
+
366
+ function sha256(buf) {
367
+ return crypto.createHash('sha256').update(buf).digest('hex');
368
+ }
369
+
370
+ /** 递归扫描 doc 目录(排除 _inbox 子目录与状态/配置文件),返回 relpath → {abs,sha256,mtime,size} */
371
+ function scanDocDir(config, configDir) {
372
+ const root = resolveDocDir(config, configDir);
373
+ const result = {};
374
+ function walk(dir, rel) {
375
+ let entries;
376
+ try {
377
+ entries = fs.readdirSync(dir, { withFileTypes: true });
378
+ } catch {
379
+ return; // 目录不存在
380
+ }
381
+ for (const e of entries) {
382
+ const abs = path.join(dir, e.name);
383
+ const relp = rel ? `${rel}/${e.name}` : e.name;
384
+ if (e.isDirectory()) {
385
+ if (e.name === '_inbox') continue; // 拉取镜像目录,不回推
386
+ walk(abs, relp);
387
+ } else if (e.isFile()) {
388
+ if (e.name === '.a2a-state.json' || e.name === '.a2a.json') continue;
389
+ const buf = fs.readFileSync(abs);
390
+ result[relp] = {
391
+ abs,
392
+ sha256: sha256(buf),
393
+ mtime: Math.floor(fs.statSync(abs).mtimeMs),
394
+ size: buf.length,
395
+ };
396
+ }
397
+ }
398
+ }
399
+ walk(root, '');
400
+ return result;
401
+ }
402
+
403
+ /** 计算本地 → 平台 的增量计划(新增/修改 + 删除) */
404
+ function computePushPlan(scan, manifest) {
405
+ const toPush = [];
406
+ const toDelete = [];
407
+ for (const [rel, info] of Object.entries(scan)) {
408
+ const prev = manifest[rel];
409
+ if (!prev || prev.sha256 !== info.sha256) toPush.push(rel);
410
+ }
411
+ for (const rel of Object.keys(manifest)) {
412
+ if (!(rel in scan)) toDelete.push(rel);
413
+ }
414
+ return { toPush, toDelete };
415
+ }
416
+
417
+ function printConflicts(conflicts) {
418
+ if (!conflicts || !conflicts.length) return;
419
+ for (const c of conflicts) {
420
+ if (c.message) console.log(paint(C.yellow, `⚠ 冲突: ${c.message}`));
421
+ else if (c.name) console.log(paint(C.yellow, `⚠ 冲突: ${c.name} 已保留平台版本,副本另存 ${c.savedAs || '(未知)'}`));
422
+ else console.log(paint(C.yellow, `⚠ 冲突: ${JSON.stringify(c)}`));
423
+ }
424
+ }
425
+
426
+ /** 推送:扫描 doc 目录 → 对比 manifest → FormData 上传 files + deletes。会就地更新 state。 */
427
+ async function pushOnce(config, configDir, state) {
428
+ const manifest = state.manifest;
429
+ const scan = scanDocDir(config, configDir);
430
+ const { toPush, toDelete } = computePushPlan(scan, manifest);
431
+
432
+ if (toPush.length === 0 && toDelete.length === 0) {
433
+ console.log(paint(C.dim, '[同步·推送] 本地无变更'));
434
+ return { pushed: [], deleted: [], conflicts: [] };
435
+ }
436
+
437
+ const form = new FormData();
438
+ for (const rel of toPush) {
439
+ const buf = fs.readFileSync(scan[rel].abs);
440
+ form.append('files', new Blob([buf]), rel);
441
+ }
442
+ if (toDelete.length) form.append('deletes', JSON.stringify(toDelete));
443
+
444
+ const res = await api(config, 'POST', '/sync', { form });
445
+ const pushed = res.pushed || [];
446
+ const deleted = res.deleted || [];
447
+ const conflicts = res.conflicts || [];
448
+
449
+ console.log(paint(C.bold, `[同步·推送] 上传 ${pushed.length} 个文件,删除 ${deleted.length} 个`));
450
+ for (const p of pushed) console.log(` + ${p.name || p}`);
451
+ for (const d of deleted) console.log(` - ${d}`);
452
+ printConflicts(conflicts);
453
+
454
+ // 仅当推送成功后再更新本地 manifest
455
+ for (const rel of toPush) {
456
+ manifest[rel] = { sha256: scan[rel].sha256, mtime: scan[rel].mtime, size: scan[rel].size };
457
+ }
458
+ for (const rel of toDelete) delete manifest[rel];
459
+ if (res.cursor) state.lastSync = Math.max(state.lastSync || 0, res.cursor);
460
+
461
+ return { pushed, deleted, conflicts };
462
+ }
463
+
464
+ /** 拉取:GET /sync?since= → 写 _inbox/<accountId>/<name>。会就地更新 state。 */
465
+ async function pullOnce(config, configDir, state) {
466
+ const since = state.lastSync || 0;
467
+ const res = await api(config, 'GET', '/sync', { query: { since } });
468
+ const changes = res.changes || [];
469
+ const docRoot = resolveDocDir(config, configDir);
470
+ let written = 0;
471
+ let removed = 0;
472
+
473
+ for (const ch of changes) {
474
+ const accountId = String(ch.accountId || 'unknown').replace(/\.\./g, '_');
475
+ const name = String(ch.name || ch.id || 'file').replace(/\.\./g, '_');
476
+ const file = path.join(docRoot, '_inbox', accountId, name);
477
+ if (ch.deleted) {
478
+ if (fs.existsSync(file)) {
479
+ fs.unlinkSync(file);
480
+ removed++;
481
+ }
482
+ } else {
483
+ fs.mkdirSync(path.dirname(file), { recursive: true });
484
+ let buf;
485
+ if (ch.content != null && ch.content !== '') {
486
+ buf = Buffer.from(ch.content, 'base64');
487
+ } else {
488
+ const dl = await api(config, 'GET', `/documents/${ch.id}/content`, { raw: true });
489
+ buf = dl.buf;
490
+ }
491
+ fs.writeFileSync(file, buf);
492
+ written++;
493
+ }
494
+ }
495
+
496
+ const cursor = Math.max(res.cursor || 0, res.time || 0);
497
+ if (cursor > (state.lastSync || 0)) state.lastSync = cursor;
498
+
499
+ console.log(paint(C.bold, `[同步·拉取] 拉取 ${written} 个文档${removed ? `,删除 ${removed} 个` : ''}`));
500
+ printConflicts(res.conflicts);
501
+ return { written, removed, changes, conflicts: res.conflicts || [] };
502
+ }
503
+
504
+ /** 双向镜像同步(先推后拉) */
505
+ async function doSync(config, configDir) {
506
+ const state = loadState(configDir);
507
+ await pushOnce(config, configDir, state);
508
+ await pullOnce(config, configDir, state);
509
+ saveState(configDir, state);
510
+ return state;
511
+ }
512
+
513
+ /* ------------------------------------------------------------------------- *
514
+ * 命令实现
515
+ * ------------------------------------------------------------------------- */
516
+
517
+ /** 交互式提问:依次向用户询问缺失的字段(仅 TTY 下启用) */
518
+ function createLineReader() {
519
+ // 自研逐行读取:输入提前到达时缓存到队列,等待者按序消费(兼容人机/伪终端/管道)
520
+ let buffer = '';
521
+ const queue = [];
522
+ const waiters = [];
523
+ process.stdin.setEncoding('utf8');
524
+ process.stdin.resume();
525
+ process.stdin.on('data', (chunk) => {
526
+ buffer += chunk;
527
+ let i;
528
+ while ((i = buffer.indexOf('\n')) >= 0) {
529
+ const line = buffer.slice(0, i).replace(/\r$/, '');
530
+ buffer = buffer.slice(i + 1);
531
+ const w = waiters.shift();
532
+ if (w) w(line);
533
+ else queue.push(line);
534
+ }
535
+ });
536
+ return function nextLine() {
537
+ if (queue.length) return Promise.resolve(queue.shift());
538
+ return new Promise((resolve) => waiters.push(resolve));
539
+ };
540
+ }
541
+
542
+ async function promptInteractive(fields) {
543
+ const nextLine = createLineReader();
544
+ const answers = {};
545
+ for (const f of fields) {
546
+ const def = f.default;
547
+ let val = '';
548
+ for (let attempt = 0; attempt < 3; attempt++) {
549
+ process.stdout.write(f.prompt + (def !== undefined && def !== '' ? `(默认: ${def}): ` : ': '));
550
+ const raw = (await nextLine()).trim();
551
+ val = raw || (def !== undefined ? def : '');
552
+ if (val || !f.required) break;
553
+ if (attempt < 2) process.stdout.write(paint(C.yellow, `「${f.key}」为必填项,请重新输入:\n`));
554
+ }
555
+ answers[f.key] = val;
556
+ }
557
+ return answers;
558
+ }
559
+
560
+ async function cmdInit(opts) {
561
+ let url = opts.url;
562
+ let name = opts.name;
563
+ let tool = opts.tool;
564
+ let project = opts.project;
565
+ let description = opts.description;
566
+ let docDir = opts['doc-dir'];
567
+
568
+ // 交互模式:终端下且必填参数缺失时,逐个提问(已通过 --xxx 提供的跳过)
569
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
570
+ if (interactive && (!url || !name || !tool || !project)) {
571
+ console.log(paint(C.bold, 'a2a init —— 交互式注册向导(输入值后回车;可 Ctrl+C 取消)'));
572
+ console.log('');
573
+ const fields = [];
574
+ if (!url) fields.push({ key: 'url', prompt: '平台地址', default: 'http://127.0.0.1:3081' });
575
+ if (!name) fields.push({ key: 'name', prompt: '账号名(端+项目,全局唯一,如 A项目开发)', required: true });
576
+ if (!tool) fields.push({ key: 'tool', prompt: '工具类型', default: 'cursor' });
577
+ if (!project) fields.push({ key: 'project', prompt: '项目名称', required: true });
578
+ if (description === undefined) fields.push({ key: 'description', prompt: '一句话简介(可回车跳过)', default: '' });
579
+ if (!docDir) fields.push({ key: 'docDir', prompt: '文档同步目录(项目内任意目录,如 docs/ 或 .a2a/docs)', default: '.a2a/docs' });
580
+ const ans = await promptInteractive(fields);
581
+ if (!ans.url && !url) return fail('未提供平台地址(--url),已取消注册。');
582
+ if (!ans.name && !name) return fail('未提供账号名(--name),已取消注册。');
583
+ url = url || ans.url;
584
+ name = name || ans.name;
585
+ tool = tool || ans.tool;
586
+ project = project || ans.project;
587
+ if (description === undefined) description = ans.description;
588
+ if (!docDir) docDir = ans.docDir;
589
+ }
590
+
591
+ if (!url) fail('init 需要 --url <平台地址>');
592
+ if (!name) fail('init 需要 --name <账号名(端+项目,全局唯一)>');
593
+ if (!tool) fail('init 需要 --tool <dsh|cursor|claude-code|other>');
594
+ if (!project) fail('init 需要 --project <项目名>');
595
+
596
+ const capabilities = splitList(opts.capabilities);
597
+ const tech = splitList(opts.tech);
598
+ if (!docDir) docDir = '.a2a/docs';
599
+ docDir = String(docDir).replace(/\/+$/, '') || '.a2a/docs';
600
+
601
+ const body = { name, tool, projectName: project, docDir };
602
+ if (description) body.description = description;
603
+ if (capabilities.length) body.capabilities = capabilities;
604
+ if (tech.length) body.tech = tech;
605
+
606
+ console.log(paint(C.bold, '正在注册账号...'));
607
+ const res = await api({ url }, 'POST', '/register', { body });
608
+
609
+ const accountId = res.accountId || name;
610
+ const token = res.token;
611
+ const cwd = process.cwd();
612
+ const configFile = path.join(cwd, '.a2a.json');
613
+ if (fs.existsSync(configFile)) {
614
+ fail(`已存在 .a2a.json(${configFile}),请先备份或删除后再 init,以免覆盖已有 token。`);
615
+ }
616
+
617
+ const config = { url: String(url).replace(/\/+$/, ''), accountId, token, docDir };
618
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2) + '\n');
619
+ saveState(cwd, defaultState());
620
+
621
+ const docRoot = resolveDocDir(config, cwd);
622
+ fs.mkdirSync(docRoot, { recursive: true });
623
+
624
+ console.log('');
625
+ console.log(paint(C.green, '注册成功'));
626
+ console.log(` 账号 (accountId): ${hl(accountId)}`);
627
+ console.log(` 平台地址: ${config.url}`);
628
+ console.log(` 文档目录: ${docRoot}`);
629
+ console.log(` token: ${paint(C.yellow, token)}(仅此一次显示,请妥善保存)`);
630
+ console.log(` 配置文件: ${configFile}`);
631
+ console.log('');
632
+ console.log(paint(C.dim, '提示: 请将 .a2a.json 与 .a2a-state.json 加入 .gitignore。'));
633
+
634
+ // 立即全量推送 doc 目录内文件(首次,manifest 为空 → 全部为新增)
635
+ const state = loadState(cwd);
636
+ await pushOnce(config, cwd, state);
637
+ saveState(cwd, state);
638
+ }
639
+
640
+ async function cmdWhoami(ctx) {
641
+ const res = await api(ctx.config, 'GET', `/agents/${ctx.config.accountId}`);
642
+ console.log(paint(C.bold, '账号信息'));
643
+ console.log(` ID: ${res.id || res.name || '-'}`);
644
+ console.log(` 名称: ${res.name || '-'}`);
645
+ console.log(` 工具: ${res.tool || '-'}`);
646
+ console.log(` 项目: ${res.project || '-'}`);
647
+ console.log(` 简介: ${res.description || '-'}`);
648
+ console.log(` 能力: ${(res.capabilities || []).join(', ') || '-'}`);
649
+ console.log(` 技术栈: ${(res.tech || []).join(', ') || '-'}`);
650
+ console.log(` 在线: ${res.online ? '是' : '否'}(${res.status || '-'}${res.note ? ' · ' + res.note : ''})`);
651
+ console.log(` 最后活跃: ${fmtTime(res.lastSeen)}`);
652
+ console.log(` 文档数: ${res.docCount ?? 0}`);
653
+ console.log(` 任务统计: ${fmtTaskStats(res.taskStats)}`);
654
+ if (res.memory) console.log(` 记忆版本: v${res.memory.version ?? 0}`);
655
+ }
656
+
657
+ async function cmdAgents(ctx) {
658
+ const list = await api(ctx.config, 'GET', '/agents');
659
+ const items = Array.isArray(list) ? list : (list && list.agents) || [];
660
+ if (items.length === 0) {
661
+ console.log('(平台上暂无账号)');
662
+ return;
663
+ }
664
+ const headers = ['名称', '工具', '项目', '在线', '最后活跃', '任务统计(待/做/阻/完)'];
665
+ const rows = items.map((a) => [
666
+ a.name || a.id || '-',
667
+ a.tool || '-',
668
+ a.project || '-',
669
+ a.online ? '在线' : '离线',
670
+ fmtAgo(a.lastSeen),
671
+ fmtTaskStats(a.taskStats),
672
+ ]);
673
+ console.log(renderTable(headers, rows));
674
+ }
675
+
676
+ async function cmdCheckin(opts, ctx) {
677
+ const { config, dir } = ctx;
678
+ const state = loadState(dir);
679
+
680
+ // 1) 双向镜像同步(先 push 再 pull)
681
+ await pushOnce(config, dir, state);
682
+ await pullOnce(config, dir, state);
683
+
684
+ // 2) 组合报到(自带 status=starting 心跳)
685
+ const since = state.lastSeq || 0;
686
+ const res = await api(config, 'GET', '/checkin', { query: { since } });
687
+
688
+ // 3) 更新 check-in 游标
689
+ const cursor = Math.max(res.time || 0, (res.inbox && res.inbox.cursor) || 0, (res.tasks && res.tasks.cursor) || 0);
690
+ state.lastSeq = Math.max(state.lastSeq || 0, cursor);
691
+ saveState(dir, state);
692
+
693
+ // 可选:显式指定状态时,追加一次心跳(checkin 接口默认 starting)
694
+ if (opts.status) {
695
+ await api(config, 'POST', '/heartbeat', { body: { status: opts.status } });
696
+ }
697
+
698
+ // 4) 输出摘要
699
+ const mem = res.memory || {};
700
+ const pending = res.pending || {};
701
+ const inboxItems = (res.inbox && res.inbox.items) || [];
702
+ const taskItems = (res.tasks && res.tasks.items) || [];
703
+ const acct = res.account || {};
704
+
705
+ console.log('');
706
+ console.log(hl('========== a2a checkin =========='));
707
+ console.log(`账号: ${acct.name || acct.id || ctx.config.accountId} 状态: ${acct.status || 'starting'}`);
708
+ console.log(`记忆版本: v${mem.version ?? 0}`);
709
+ console.log(`未读消息: ${pending.unreadMessages ?? inboxItems.length} 条 待办任务: ${pending.todoTasks ?? taskItems.length} 个`);
710
+
711
+ console.log('');
712
+ console.log(paint(C.bold, '未读消息:'));
713
+ if (inboxItems.length === 0) {
714
+ console.log(' (无)');
715
+ } else {
716
+ inboxItems.forEach((m, i) => console.log(` [${i + 1}] ${m.subject || '(无主题)'} — 来自 ${m.from || '?'}`));
717
+ }
718
+
719
+ console.log('');
720
+ console.log(paint(C.bold, '待办任务:'));
721
+ if (taskItems.length === 0) {
722
+ console.log(' (无)');
723
+ } else {
724
+ taskItems.forEach((t, i) => console.log(` [${i + 1}] ${t.title || '(无标题)'}(${t.status || '?'})`));
725
+ }
726
+
727
+ console.log('');
728
+ if ((pending.unreadMessages ?? inboxItems.length) > 0) {
729
+ console.log(paint(C.yellow, '→ 有未读消息:用 a2a inbox --unread 查看'));
730
+ }
731
+ if ((pending.todoTasks ?? taskItems.length) > 0) {
732
+ console.log(paint(C.yellow, '→ 有待办任务:用 a2a task list --status todo 查看'));
733
+ }
734
+ console.log(hl('======================================='));
735
+ }
736
+
737
+ async function cmdSend(opts, ctx) {
738
+ const to = opts.to;
739
+ const subject = opts.subject;
740
+ const body = opts.body;
741
+ if (!to) fail('send 需要 --to <收件账号名>');
742
+ if (!subject) fail('send 需要 --subject <主题>');
743
+ if (body === undefined || body === null) fail('send 需要 --body <正文>');
744
+
745
+ const payload = { to, subject, body };
746
+ if (opts.priority) payload.priority = opts.priority;
747
+ if (opts['need-reply']) payload.needsReply = true;
748
+ const docs = listOpt(opts.doc);
749
+ if (docs.length) payload.docIds = docs;
750
+
751
+ const res = await api(ctx.config, 'POST', '/messages', { body: payload });
752
+ const id = res.messageId || res.id;
753
+ console.log(`已发送消息 ${hl(id)} → ${to}(主题: ${subject})`);
754
+ }
755
+
756
+ async function cmdInbox(opts, ctx, dir) {
757
+ const query = { dir };
758
+ if (opts.unread) query.status = 'unread';
759
+ if (opts.limit) query.limit = opts.limit;
760
+ const res = await api(ctx.config, 'GET', '/messages', { query });
761
+ const items = toArray(res);
762
+ if (items.length === 0) {
763
+ console.log('(无消息)');
764
+ return;
765
+ }
766
+ const headers = dir === 'in' ? ['ID', '编号', '来自', '主题', '状态', '时间'] : ['ID', '编号', '发给', '主题', '状态', '时间'];
767
+ const rows = items.map((m, i) => {
768
+ const peer = dir === 'in' ? m.from : m.to;
769
+ return [m.id || '-', String(i + 1), peer || '-', m.subject || '-', m.status || '-', fmtTime(m.createdAt)];
770
+ });
771
+ console.log(renderTable(headers, rows));
772
+ }
773
+
774
+ async function cmdReply(opts, ctx) {
775
+ const id = opts.msg;
776
+ const body = opts.body;
777
+ if (!id) fail('reply 需要 --msg <消息ID>');
778
+ if (body === undefined || body === null) fail('reply 需要 --body <正文>');
779
+
780
+ const payload = { body };
781
+ const docs = listOpt(opts.doc);
782
+ if (docs.length) payload.docIds = docs;
783
+
784
+ const res = await api(ctx.config, 'POST', `/messages/${id}/reply`, { body: payload });
785
+ const mid = res.messageId || res.id || '';
786
+ console.log(`已回复消息 ${id}${mid ? ` → 新消息 ${hl(mid)}` : ''}`);
787
+ }
788
+
789
+ async function cmdMark(opts, ctx) {
790
+ const id = opts.msg;
791
+ const status = opts.status;
792
+ if (!id) fail('mark 需要 --msg <消息ID>');
793
+ if (!status) fail('mark 需要 --status <unread|read|processing|resolved>');
794
+ await api(ctx.config, 'POST', `/messages/${id}/status`, { body: { status } });
795
+ console.log(`消息 ${id} 已标记为 ${status}`);
796
+ }
797
+
798
+ async function cmdTaskNew(opts, ctx) {
799
+ const title = opts.title;
800
+ if (!title) fail('task new 需要 --title <标题>');
801
+ const payload = { title };
802
+ if (opts.desc) payload.description = opts.desc;
803
+ if (opts.assignee) payload.assigneeId = opts.assignee;
804
+ if (opts.priority) payload.priority = opts.priority;
805
+ if (opts['source-msg']) payload.sourceMessageId = opts['source-msg'];
806
+ const res = await api(ctx.config, 'POST', '/tasks', { body: payload });
807
+ console.log(`已创建任务 ${hl(res.taskId || res.id || '')}`);
808
+ }
809
+
810
+ async function cmdTaskList(opts, ctx) {
811
+ const query = {};
812
+ if (opts.status) query.status = opts.status;
813
+ if (opts.account) query.account = opts.account;
814
+ const res = await api(ctx.config, 'GET', '/tasks', { query });
815
+ const items = toArray(res);
816
+ if (items.length === 0) {
817
+ console.log('(无任务)');
818
+ return;
819
+ }
820
+ const headers = ['ID', '编号', '标题', '状态', '优先级', '负责人', '更新时间'];
821
+ const rows = items.map((t, i) => [
822
+ t.id || '-',
823
+ String(i + 1),
824
+ t.title || '-',
825
+ t.status || '-',
826
+ t.priority || '-',
827
+ t.assigneeId || '-',
828
+ fmtTime(t.updatedAt),
829
+ ]);
830
+ console.log(renderTable(headers, rows));
831
+ }
832
+
833
+ async function cmdTaskUpdate(opts, ctx) {
834
+ const id = opts.id;
835
+ if (!id) fail('task update 需要 --id <任务ID>');
836
+ const payload = {};
837
+ if (opts.status) payload.status = opts.status;
838
+ if (opts.note) payload.note = opts.note;
839
+ if (opts.assignee) payload.assigneeId = opts.assignee;
840
+ if (Object.keys(payload).length === 0) fail('task update 至少需要 --status / --note / --assignee 之一');
841
+ await api(ctx.config, 'PATCH', `/tasks/${id}`, { body: payload });
842
+ console.log(`任务 ${id} 已更新`);
843
+ }
844
+
845
+ async function cmdDocUp(opts, ctx, pos) {
846
+ const file = pos[0];
847
+ if (!file) fail('doc up 需要 <文件路径>');
848
+ const abs = path.resolve(process.cwd(), file);
849
+ if (!fs.existsSync(abs)) fail(`文件不存在: ${file}`);
850
+ const buf = fs.readFileSync(abs);
851
+ const form = new FormData();
852
+ form.append('file', new Blob([buf]), path.basename(abs));
853
+ if (opts.desc) form.append('description', opts.desc);
854
+ const res = await api(ctx.config, 'POST', '/documents', { form });
855
+ const doc = res.document || res;
856
+ console.log(`已上传文档 ${hl(doc.id || '')}(${doc.name || path.basename(abs)},${fmtSize(doc.size ?? buf.length)})`);
857
+ }
858
+
859
+ async function cmdDocLs(opts, ctx) {
860
+ const query = {};
861
+ if (opts.account) query.account = opts.account;
862
+ const res = await api(ctx.config, 'GET', '/documents', { query });
863
+ const items = toArray(res);
864
+ if (items.length === 0) {
865
+ console.log('(无文档)');
866
+ return;
867
+ }
868
+ const headers = ['ID', '名称', '账号', '大小', '时间'];
869
+ const rows = items.map((d) => [d.id || '-', d.name || '-', d.accountId || '-', fmtSize(d.size), fmtTime(d.createdAt)]);
870
+ console.log(renderTable(headers, rows));
871
+ }
872
+
873
+ async function cmdDocGet(opts, ctx, pos) {
874
+ const id = pos[0];
875
+ if (!id) fail('doc get 需要 <文档ID>');
876
+
877
+ const dl = await api(ctx.config, 'GET', `/documents/${id}/content`, {
878
+ raw: true,
879
+ query: opts.inline ? { inline: 1 } : undefined,
880
+ });
881
+ const buf = dl.buf;
882
+
883
+ if (opts.inline) {
884
+ const text = buf.toString('utf8');
885
+ process.stdout.write(text);
886
+ if (text && !text.endsWith('\n')) process.stdout.write('\n');
887
+ return;
888
+ }
889
+
890
+ let out = opts.out;
891
+ if (!out) {
892
+ let name = id;
893
+ try {
894
+ const meta = await api(ctx.config, 'GET', `/documents/${id}`);
895
+ if (meta && meta.name) name = meta.name;
896
+ } catch {
897
+ /* 元数据获取失败时用 id 兜底 */
898
+ }
899
+ out = name;
900
+ }
901
+ const outAbs = path.resolve(process.cwd(), out);
902
+ fs.mkdirSync(path.dirname(outAbs), { recursive: true });
903
+ fs.writeFileSync(outAbs, buf);
904
+ console.log(`已保存到 ${outAbs}(${fmtSize(buf.length)})`);
905
+ }
906
+
907
+ async function cmdMemoryGet(ctx) {
908
+ const res = await api(ctx.config, 'GET', '/memory');
909
+ const version = res.version ?? 0;
910
+ const content = res.content ?? '';
911
+ console.log(`记忆版本: v${version}`);
912
+ console.log('--- memory.md ---');
913
+ process.stdout.write(content);
914
+ if (content && !content.endsWith('\n')) process.stdout.write('\n');
915
+ }
916
+
917
+ async function cmdMemorySet(ctx, pos) {
918
+ const file = pos[0];
919
+ if (!file) fail('memory set 需要 <文件路径>');
920
+ const abs = path.resolve(process.cwd(), file);
921
+ if (!fs.existsSync(abs)) fail(`文件不存在: ${file}`);
922
+ const content = fs.readFileSync(abs, 'utf8');
923
+
924
+ const cur = await api(ctx.config, 'GET', '/memory');
925
+ const version = cur.version ?? 0;
926
+
927
+ try {
928
+ const res = await api(ctx.config, 'PUT', '/memory', { body: { content, version } });
929
+ const newVer = res.version != null ? res.version : version + 1;
930
+ console.log(`已更新记忆到 v${newVer}`);
931
+ } catch (e) {
932
+ if (e instanceof ApiError && e.status === 409) {
933
+ fail(`记忆版本冲突(当前平台版本 v${cur.version ?? '?'})。请先 a2a memory get 获取最新内容并合并,再重新 a2a memory set。`);
934
+ }
935
+ throw e;
936
+ }
937
+ }
938
+
939
+ async function cmdHeartbeat(opts, ctx) {
940
+ const payload = {};
941
+ if (opts.status) payload.status = opts.status;
942
+ if (opts.note) payload.note = opts.note;
943
+ const res = await api(ctx.config, 'POST', '/heartbeat', { body: payload });
944
+ const pending = res.pending || {};
945
+ console.log(
946
+ `心跳成功:在线=${res.online ? '是' : '否'} 状态=${res.status || opts.status || '-'} ` +
947
+ `未读=${pending.unreadMessages ?? 0} 待办=${pending.todoTasks ?? 0}`
948
+ );
949
+ }
950
+
951
+ /* ------------------------------------------------------------------------- *
952
+ * 帮助
953
+ * ------------------------------------------------------------------------- */
954
+
955
+ function printHelp() {
956
+ console.log(hl('Agent2Agent 统一 CLI · a2a'));
957
+ console.log('Agent ↔ Agent 异步协作平台的零依赖接入工具。');
958
+ console.log('');
959
+ console.log(paint(C.bold, '用法:') + ' a2a <命令> [选项]');
960
+ console.log('');
961
+ console.log(paint(C.bold, '命令:'));
962
+ const cmds = [
963
+ ['init', '注册账号、生成配置并全量推送文档目录'],
964
+ ['whoami', '查看当前账号信息'],
965
+ ['agents', '查看平台目录(谁是谁、在做什么)'],
966
+ ['checkin', '启动报到:双向同步 + 拉取收件箱/待办/记忆摘要'],
967
+ ['send', '发送消息'],
968
+ ['inbox', '收件箱'],
969
+ ['outbox', '发件箱'],
970
+ ['reply', '回复消息'],
971
+ ['mark', '标记消息状态'],
972
+ ['task', '任务看板(new / list / update)'],
973
+ ['doc', '文档(up / ls / get)'],
974
+ ['sync', '双向镜像同步本地 doc 目录 ↔ 平台'],
975
+ ['memory', '记忆(get / set)'],
976
+ ['heartbeat', '心跳'],
977
+ ['help', '显示本帮助'],
978
+ ];
979
+ for (const [c, d] of cmds) console.log(` ${paint(C.cyan, c.padEnd(10))} ${d}`);
980
+
981
+ console.log('');
982
+ console.log(paint(C.bold, '全局选项:'));
983
+ console.log(' --config <path> 指定配置文件路径(默认从当前目录逐级向上查找 .a2a.json)');
984
+
985
+ console.log('');
986
+ console.log(paint(C.bold, '命令用法:'));
987
+ console.log(' a2a init --url <U> --name <N> --tool <T> --project <P> [--description D] [--capabilities a,b] [--tech x,y] [--doc-dir D]');
988
+ console.log(' a2a whoami');
989
+ console.log(' a2a agents');
990
+ console.log(' a2a checkin [--status S]');
991
+ console.log(' a2a send --to <X> --subject <S> --body <B> [--doc id]... [--need-reply] [--priority P]');
992
+ console.log(' a2a inbox [--unread] [--limit N]');
993
+ console.log(' a2a outbox [--limit N]');
994
+ console.log(' a2a reply --msg <ID> --body <B> [--doc id]...');
995
+ console.log(' a2a mark --msg <ID> --status <S>');
996
+ console.log(' a2a task new --title <T> [--desc D] [--assignee A] [--priority P] [--source-msg M]');
997
+ console.log(' a2a task list [--status S] [--account A]');
998
+ console.log(' a2a task update --id <ID> [--status S] [--note N] [--assignee A]');
999
+ console.log(' a2a doc up <file> [--desc D]');
1000
+ console.log(' a2a doc ls [--account A]');
1001
+ console.log(' a2a doc get <id> [--out FILE] [--inline]');
1002
+ console.log(' a2a sync');
1003
+ console.log(' a2a memory get');
1004
+ console.log(' a2a memory set <file>');
1005
+ console.log(' a2a heartbeat [--status S] [--note N]');
1006
+
1007
+ console.log('');
1008
+ console.log(paint(C.bold, '示例:'));
1009
+ console.log(' a2a init --url http://127.0.0.1:3081 --name A项目开发 --tool cursor --project A项目');
1010
+ console.log(' a2a checkin');
1011
+ console.log(' a2a send --to B项目开发 --subject 需要API --body "请提供接口清单" --need-reply');
1012
+ console.log(' a2a inbox --unread');
1013
+ console.log(' a2a task new --title 实现登录 --priority high');
1014
+ console.log(' a2a doc up ./需求.md');
1015
+ console.log(' a2a sync');
1016
+ }
1017
+
1018
+ /* ------------------------------------------------------------------------- *
1019
+ * 入口
1020
+ * ------------------------------------------------------------------------- */
1021
+
1022
+ async function main() {
1023
+ const { pos, opts } = parseArgs(process.argv.slice(2));
1024
+ const cmd = pos[0];
1025
+
1026
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
1027
+ printHelp();
1028
+ return;
1029
+ }
1030
+
1031
+ if (cmd === 'init') {
1032
+ await cmdInit(opts);
1033
+ return;
1034
+ }
1035
+
1036
+ const ctx = requireConfig(opts.config);
1037
+ const sub = pos[1];
1038
+
1039
+ switch (cmd) {
1040
+ case 'whoami':
1041
+ await cmdWhoami(ctx);
1042
+ break;
1043
+ case 'agents':
1044
+ await cmdAgents(ctx);
1045
+ break;
1046
+ case 'checkin':
1047
+ await cmdCheckin(opts, ctx);
1048
+ break;
1049
+ case 'send':
1050
+ await cmdSend(opts, ctx);
1051
+ break;
1052
+ case 'inbox':
1053
+ await cmdInbox(opts, ctx, 'in');
1054
+ break;
1055
+ case 'outbox':
1056
+ await cmdInbox(opts, ctx, 'out');
1057
+ break;
1058
+ case 'reply':
1059
+ await cmdReply(opts, ctx);
1060
+ break;
1061
+ case 'mark':
1062
+ await cmdMark(opts, ctx);
1063
+ break;
1064
+ case 'sync':
1065
+ await doSync(ctx.config, ctx.dir);
1066
+ break;
1067
+ case 'heartbeat':
1068
+ await cmdHeartbeat(opts, ctx);
1069
+ break;
1070
+ case 'task':
1071
+ if (sub === 'new') await cmdTaskNew(opts, ctx);
1072
+ else if (sub === 'list') await cmdTaskList(opts, ctx);
1073
+ else if (sub === 'update') await cmdTaskUpdate(opts, ctx);
1074
+ else fail('task 子命令: new | list | update(用 a2a help 查看用法)');
1075
+ break;
1076
+ case 'doc':
1077
+ if (sub === 'up') await cmdDocUp(opts, ctx, pos.slice(2));
1078
+ else if (sub === 'ls') await cmdDocLs(opts, ctx);
1079
+ else if (sub === 'get') await cmdDocGet(opts, ctx, pos.slice(2));
1080
+ else fail('doc 子命令: up | ls | get(用 a2a help 查看用法)');
1081
+ break;
1082
+ case 'memory':
1083
+ if (sub === 'get') await cmdMemoryGet(ctx);
1084
+ else if (sub === 'set') await cmdMemorySet(ctx, pos.slice(2));
1085
+ else fail('memory 子命令: get | set(用 a2a help 查看用法)');
1086
+ break;
1087
+ default:
1088
+ fail(`未知命令: ${cmd}(用 a2a help 查看全部命令)`);
1089
+ }
1090
+ }
1091
+
1092
+ main().catch((err) => {
1093
+ const msg = err && err.message ? err.message : String(err);
1094
+ fail(msg);
1095
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "agent2agent-cli",
3
+ "version": "0.2.0",
4
+ "description": "Agent2Agent 统一 CLI(命令名 a2a):跨 AI 编程代理协作平台的命令行客户端 — 异步消息、任务看板、文档双向同步、持久记忆",
5
+ "bin": {
6
+ "a2a": "a2a.js"
7
+ },
8
+ "files": [
9
+ "a2a.js"
10
+ ],
11
+ "keywords": [
12
+ "ai-agents",
13
+ "multi-agent",
14
+ "collaboration",
15
+ "agent2agent",
16
+ "a2a",
17
+ "cli"
18
+ ],
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/BajaXX/Agent2Agent.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/BajaXX/Agent2Agent/issues"
26
+ },
27
+ "homepage": "https://github.com/BajaXX/Agent2Agent#readme",
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }