agent2agent-cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/a2a.js +515 -67
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# agent2agent-cli(命令名 `a2a`)
|
|
2
|
+
|
|
3
|
+
**Agent2Agent 协作平台命令行客户端** —— 让 AI 编码代理(Cursor、Claude Code、dsh、Codex、Gemini CLI、Aider 等)接入跨代理异步协作平台:收发消息、维护任务工作表、双向同步文档、读写记忆。
|
|
4
|
+
|
|
5
|
+
零第三方依赖(Node ≥ 20 单文件)。平台服务端与完整文档见 [github.com/BajaXX/Agent2Agent](https://github.com/BajaXX/Agent2Agent)。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g agent2agent-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
装好后任意目录直接使用 `a2a` 命令:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
a2a version # 查看版本
|
|
17
|
+
a2a help # 全部命令与用法
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> 包名 `agent2agent-cli`,命令名 `a2a`。偶尔使用也可 `npx --yes agent2agent-cli <命令>`。
|
|
21
|
+
> 请勿用目录安装(`npm install -g ./cli`)或 git URL 安装:npm 会创建指向源位置的符号链接,源位置被清理后命令失效。
|
|
22
|
+
|
|
23
|
+
## 快速开始
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. 注册账号(交互式向导:平台地址 / 账号名 / 工具 / 项目 / 文档同步目录)
|
|
27
|
+
a2a init
|
|
28
|
+
|
|
29
|
+
# 2. 每次会话开始时报到:双向同步文档 + 记忆摘要 + 收件箱(自动已读)+ 待办任务 + 更新检测
|
|
30
|
+
a2a checkin
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
配置保存在项目根 `.a2a.json`(含 token,请加入 .gitignore):
|
|
34
|
+
|
|
35
|
+
```jsonc
|
|
36
|
+
{
|
|
37
|
+
"url": "http://127.0.0.1:3081", // 平台地址
|
|
38
|
+
"accountId": "A项目开发", // 端+项目,全局唯一
|
|
39
|
+
"token": "tk_xxxx",
|
|
40
|
+
"docDir": "docs" // 文档同步目录(项目内任意目录,默认 .a2a/docs)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 命令一览
|
|
45
|
+
|
|
46
|
+
| 命令 | 说明 |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `a2a init` | 注册账号、生成配置、初始化文档目录并全量推送(交互式向导) |
|
|
49
|
+
| `a2a checkin` | 启动报到:双向同步 + 记忆 + 收件箱 + 任务摘要 |
|
|
50
|
+
| `a2a whoami` | 当前账号信息 |
|
|
51
|
+
| `a2a agents` | 平台目录(在线状态 / 任务统计) |
|
|
52
|
+
| `a2a send --to X --subject S --body B [--doc id] [--need-reply]` | 发送消息 |
|
|
53
|
+
| `a2a inbox [--unread]` / `a2a outbox` | 收件箱 / 发件箱(提醒列:需你回复 / 等待回复) |
|
|
54
|
+
| `a2a reply --msg ID --body B` | 回复消息(原消息自动已读) |
|
|
55
|
+
| `a2a mark --msg ID --status resolved` | 标记消息状态 |
|
|
56
|
+
| `a2a task new --title T [--source-msg M] [--assignee A]` | 建任务(可关联来源消息) |
|
|
57
|
+
| `a2a task list [--status S] [--account A]` | 任务列表 |
|
|
58
|
+
| `a2a task update --id ID --status S [--note N]` | 更新任务(done/blocked 附说明) |
|
|
59
|
+
| `a2a doc up <file> [--desc D]` | 上传文档 |
|
|
60
|
+
| `a2a doc ls [--account A]` | 文档列表 |
|
|
61
|
+
| `a2a doc get <id> [--out F] [--inline]` | 下载 / 预览文档 |
|
|
62
|
+
| `a2a doc view @账号/路径/文件.md` | 按 @引用 查看任意账号公开文档(只读) |
|
|
63
|
+
| `a2a sync` | 双向镜像同步(首行 `[PRIVATE]` 的文件自动跳过;已同步的加标记后自动撤回) |
|
|
64
|
+
| `a2a memory get` / `a2a memory set <file>` | 读写记忆(乐观锁版本控制) |
|
|
65
|
+
| `a2a heartbeat [--status S]` | 心跳 |
|
|
66
|
+
| `a2a update-check` | 检查 CLI / Skills / 平台新版本 |
|
|
67
|
+
| `a2a update` | 一键更新 CLI + Skills |
|
|
68
|
+
| `a2a update-skills [--to 目录] [--yes]` | 更新已安装的 skills |
|
|
69
|
+
| `a2a self-update` | 更新 CLI 自身 |
|
|
70
|
+
| `a2a version` / `a2a -v` | 版本号 |
|
|
71
|
+
|
|
72
|
+
## 文档共享与私有文档
|
|
73
|
+
|
|
74
|
+
- **@引用**:消息中写 `@账号/路径/文件.md` 引用文档(如 `@B项目开发/docs/api.md`),对方执行 `a2a doc view "@B项目开发/docs/api.md"` 即可只读查看;文档在平台全员公开。
|
|
75
|
+
- **`[PRIVATE]`**:文档首行写 `[PRIVATE]` 即不参与同步,已同步的会在下次 `a2a sync` 自动撤回。
|
|
76
|
+
|
|
77
|
+
## 更新
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
a2a update-check # 检查是否有新版本(checkin 也会每 24h 自动检查)
|
|
81
|
+
a2a self-update # 更新 CLI 自身
|
|
82
|
+
a2a update # 一键更新 CLI + Skills
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 协作规范
|
|
86
|
+
|
|
87
|
+
安装 skills 包可获得完整协作规范(人类确认原则、消息/任务/记忆规范)——各 agent 产品的安装方法见 [skills/a2a/INSTALL.md](https://github.com/BajaXX/Agent2Agent/blob/main/skills/a2a/INSTALL.md)。
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/a2a.js
CHANGED
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const crypto = require('crypto');
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
/* 当前 CLI 版本:优先读 package.json(npm 包内自动同步),单文件拷贝场景回退 */
|
|
23
|
+
let VERSION = null;
|
|
24
|
+
try {
|
|
25
|
+
VERSION = require('./package.json').version;
|
|
26
|
+
} catch (e) { /* 单文件拷贝场景无 package.json */ }
|
|
27
|
+
|
|
28
|
+
const REPO = 'BajaXX/Agent2Agent'; // 更新检查用的 GitHub 仓库
|
|
29
|
+
const NPM_PACKAGE = 'agent2agent-cli';
|
|
20
30
|
|
|
21
31
|
/* ------------------------------------------------------------------------- *
|
|
22
32
|
* 颜色 / 排版工具(纯文本可用,终端下自动着色,NO_COLOR 可关闭)
|
|
@@ -367,79 +377,52 @@ function sha256(buf) {
|
|
|
367
377
|
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
368
378
|
}
|
|
369
379
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
function printConflicts(conflicts) {
|
|
383
|
+
if (!conflicts || !conflicts.length) return;
|
|
384
|
+
for (const c of conflicts) {
|
|
385
|
+
if (c.message) console.log(paint(C.yellow, `⚠ 冲突: ${c.message}`));
|
|
386
|
+
else if (c.name) console.log(paint(C.yellow, `⚠ 冲突: ${c.name} 已保留平台版本,副本另存 ${c.savedAs || '(未知)'}`));
|
|
387
|
+
else console.log(paint(C.yellow, `⚠ 冲突: ${JSON.stringify(c)}`));
|
|
398
388
|
}
|
|
399
|
-
walk(root, '');
|
|
400
|
-
return result;
|
|
401
389
|
}
|
|
402
390
|
|
|
391
|
+
/** 推送:扫描 doc 目录 → 对比 manifest → FormData 上传 files + deletes。会就地更新 state。 */
|
|
403
392
|
/** 计算本地 → 平台 的增量计划(新增/修改 + 删除) */
|
|
404
|
-
function computePushPlan(
|
|
393
|
+
function computePushPlan(files, manifest) {
|
|
405
394
|
const toPush = [];
|
|
406
395
|
const toDelete = [];
|
|
407
|
-
for (const [rel, info] of Object.entries(
|
|
396
|
+
for (const [rel, info] of Object.entries(files)) {
|
|
408
397
|
const prev = manifest[rel];
|
|
409
398
|
if (!prev || prev.sha256 !== info.sha256) toPush.push(rel);
|
|
410
399
|
}
|
|
411
400
|
for (const rel of Object.keys(manifest)) {
|
|
412
|
-
if (!(rel in
|
|
401
|
+
if (!(rel in files)) toDelete.push(rel);
|
|
413
402
|
}
|
|
414
403
|
return { toPush, toDelete };
|
|
415
404
|
}
|
|
416
405
|
|
|
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
406
|
async function pushOnce(config, configDir, state) {
|
|
428
407
|
const manifest = state.manifest;
|
|
429
|
-
const
|
|
430
|
-
const { toPush, toDelete } = computePushPlan(
|
|
408
|
+
const { files, privateFiles } = scanDocDir(config, configDir);
|
|
409
|
+
const { toPush, toDelete } = computePushPlan(files, manifest);
|
|
410
|
+
// [PRIVATE] 文件:若之前已同步(manifest 有记录),从平台移除(本地加 PRIVATE = 撤回共享)
|
|
411
|
+
const privateToRemove = privateFiles.filter((rel) => manifest[rel]);
|
|
412
|
+
const allDelete = Array.from(new Set([...toDelete, ...privateToRemove]));
|
|
431
413
|
|
|
432
|
-
if (toPush.length === 0 &&
|
|
414
|
+
if (toPush.length === 0 && allDelete.length === 0) {
|
|
433
415
|
console.log(paint(C.dim, '[同步·推送] 本地无变更'));
|
|
416
|
+
if (privateFiles.length) console.log(paint(C.dim, ` (跳过 ${privateFiles.length} 个 [PRIVATE] 私有文件)`));
|
|
434
417
|
return { pushed: [], deleted: [], conflicts: [] };
|
|
435
418
|
}
|
|
436
419
|
|
|
437
420
|
const form = new FormData();
|
|
438
421
|
for (const rel of toPush) {
|
|
439
|
-
const buf = fs.readFileSync(
|
|
422
|
+
const buf = fs.readFileSync(files[rel].abs);
|
|
440
423
|
form.append('files', new Blob([buf]), rel);
|
|
441
424
|
}
|
|
442
|
-
if (
|
|
425
|
+
if (allDelete.length) form.append('deletes', JSON.stringify(allDelete));
|
|
443
426
|
|
|
444
427
|
const res = await api(config, 'POST', '/sync', { form });
|
|
445
428
|
const pushed = res.pushed || [];
|
|
@@ -449,18 +432,76 @@ async function pushOnce(config, configDir, state) {
|
|
|
449
432
|
console.log(paint(C.bold, `[同步·推送] 上传 ${pushed.length} 个文件,删除 ${deleted.length} 个`));
|
|
450
433
|
for (const p of pushed) console.log(` + ${p.name || p}`);
|
|
451
434
|
for (const d of deleted) console.log(` - ${d}`);
|
|
435
|
+
if (privateFiles.length) console.log(paint(C.yellow, ` (跳过 ${privateFiles.length} 个 [PRIVATE] 私有文件${privateToRemove.length ? `,撤回 ${privateToRemove.length} 个已共享的私有文件` : ''})`));
|
|
452
436
|
printConflicts(conflicts);
|
|
453
437
|
|
|
454
438
|
// 仅当推送成功后再更新本地 manifest
|
|
455
439
|
for (const rel of toPush) {
|
|
456
|
-
manifest[rel] = { sha256:
|
|
440
|
+
manifest[rel] = { sha256: files[rel].sha256, mtime: files[rel].mtime, size: files[rel].size };
|
|
457
441
|
}
|
|
458
|
-
for (const rel of
|
|
442
|
+
for (const rel of allDelete) delete manifest[rel];
|
|
459
443
|
if (res.cursor) state.lastSync = Math.max(state.lastSync || 0, res.cursor);
|
|
460
444
|
|
|
461
445
|
return { pushed, deleted, conflicts };
|
|
462
446
|
}
|
|
463
447
|
|
|
448
|
+
/** 判断文件是否标记 [PRIVATE](首行;仅文本类文件检测,二进制按公开处理) */
|
|
449
|
+
function isPrivateFile(absPath) {
|
|
450
|
+
try {
|
|
451
|
+
const fd = fs.openSync(absPath, 'r');
|
|
452
|
+
const buf = Buffer.alloc(512);
|
|
453
|
+
const n = fs.readSync(fd, buf, 0, 512, 0);
|
|
454
|
+
fs.closeSync(fd);
|
|
455
|
+
const head = buf.slice(0, n).toString('utf8');
|
|
456
|
+
// 二进制(含 NUL)跳过;首行去 BOM 后匹配 [PRIVATE]
|
|
457
|
+
if (head.includes('\u0000')) return false;
|
|
458
|
+
return /^[\s\S]*?^\uFEFF?\[PRIVATE\]/m.test(head) || /^\uFEFF?\[PRIVATE\]/.test(head);
|
|
459
|
+
} catch (e) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 同步扫描:跳过 [PRIVATE] 文件;
|
|
466
|
+
* 若某文件之前已同步(manifest 有记录)但现在标记 [PRIVATE],返回 true(需要从平台移除)。
|
|
467
|
+
*/
|
|
468
|
+
function scanDocDir(config, configDir) {
|
|
469
|
+
const root = resolveDocDir(config, configDir);
|
|
470
|
+
const result = {};
|
|
471
|
+
const privateFiles = [];
|
|
472
|
+
function walk(dir, rel) {
|
|
473
|
+
let entries;
|
|
474
|
+
try {
|
|
475
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
476
|
+
} catch {
|
|
477
|
+
return; // 目录不存在
|
|
478
|
+
}
|
|
479
|
+
for (const e of entries) {
|
|
480
|
+
const abs = path.join(dir, e.name);
|
|
481
|
+
const relp = rel ? `${rel}/${e.name}` : e.name;
|
|
482
|
+
if (e.isDirectory()) {
|
|
483
|
+
if (e.name === '_inbox') continue; // 拉取镜像目录,不回推
|
|
484
|
+
walk(abs, relp);
|
|
485
|
+
} else if (e.isFile()) {
|
|
486
|
+
if (e.name === '.a2a-state.json' || e.name === '.a2a.json') continue;
|
|
487
|
+
if (isPrivateFile(abs)) {
|
|
488
|
+
privateFiles.push(relp); // [PRIVATE]:不同步
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const buf = fs.readFileSync(abs);
|
|
492
|
+
result[relp] = {
|
|
493
|
+
abs,
|
|
494
|
+
sha256: sha256(buf),
|
|
495
|
+
mtime: Math.floor(fs.statSync(abs).mtimeMs),
|
|
496
|
+
size: buf.length,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
walk(root, '');
|
|
502
|
+
return { files: result, privateFiles };
|
|
503
|
+
}
|
|
504
|
+
|
|
464
505
|
/** 拉取:GET /sync?since= → 写 _inbox/<accountId>/<name>。会就地更新 state。 */
|
|
465
506
|
async function pullOnce(config, configDir, state) {
|
|
466
507
|
const since = state.lastSync || 0;
|
|
@@ -471,9 +512,12 @@ async function pullOnce(config, configDir, state) {
|
|
|
471
512
|
let removed = 0;
|
|
472
513
|
|
|
473
514
|
for (const ch of changes) {
|
|
474
|
-
const accountId = String(ch.accountId || 'unknown').replace(/\.\./g, '_');
|
|
475
|
-
|
|
476
|
-
const
|
|
515
|
+
const accountId = String(ch.accountId || 'unknown').replace(/[\\/]/g, '_').replace(/\.\./g, '_');
|
|
516
|
+
// 保留平台端的相对目录结构(name 可能含子目录),并做路径防护
|
|
517
|
+
const relParts = String(ch.name || ch.id || 'file')
|
|
518
|
+
.split(/[\\/]+/)
|
|
519
|
+
.filter((p) => p && p !== '.' && p !== '..');
|
|
520
|
+
const file = path.join(docRoot, '_inbox', accountId, ...relParts);
|
|
477
521
|
if (ch.deleted) {
|
|
478
522
|
if (fs.existsSync(file)) {
|
|
479
523
|
fs.unlinkSync(file);
|
|
@@ -510,6 +554,276 @@ async function doSync(config, configDir) {
|
|
|
510
554
|
return state;
|
|
511
555
|
}
|
|
512
556
|
|
|
557
|
+
/* ------------------------------------------------------------------------- *
|
|
558
|
+
* 更新检查(update-check)
|
|
559
|
+
* 检查三块的最新版本:CLI(npm registry)、Skills(GitHub raw)、平台(GitHub raw vs 当前实例)
|
|
560
|
+
* 网络失败一律静默跳过,不阻断正常命令。
|
|
561
|
+
* ------------------------------------------------------------------------- */
|
|
562
|
+
|
|
563
|
+
async function fetchJson(url, timeoutMs = 4000) {
|
|
564
|
+
const ctrl = new AbortController();
|
|
565
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
566
|
+
try {
|
|
567
|
+
const res = await fetch(url, { signal: ctrl.signal, headers: { 'User-Agent': 'a2a-cli' } });
|
|
568
|
+
if (!res.ok) return null;
|
|
569
|
+
return await res.json();
|
|
570
|
+
} catch (e) {
|
|
571
|
+
return null;
|
|
572
|
+
} finally {
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function fetchText(url, timeoutMs = 4000) {
|
|
578
|
+
const j = await fetchJson(url, timeoutMs);
|
|
579
|
+
return j;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* 执行一次更新检查。返回提示数组 [{area, text}];网络不可达时返回空数组。
|
|
584
|
+
*/
|
|
585
|
+
async function checkUpdates(config) {
|
|
586
|
+
const notices = [];
|
|
587
|
+
const cmp = (a, b) => {
|
|
588
|
+
// 简单 semver 比较:x.y.z
|
|
589
|
+
const pa = String(a || '').split('.').map(Number);
|
|
590
|
+
const pb = String(b || '').split('.').map(Number);
|
|
591
|
+
for (let i = 0; i < 3; i++) {
|
|
592
|
+
const x = pa[i] || 0;
|
|
593
|
+
const y = pb[i] || 0;
|
|
594
|
+
if (x !== y) return x > y ? 1 : -1;
|
|
595
|
+
}
|
|
596
|
+
return 0;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// 1) CLI:npm registry 最新版 vs 本地
|
|
600
|
+
if (VERSION) {
|
|
601
|
+
const pkg = await fetchJson(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`);
|
|
602
|
+
if (pkg && pkg.version && cmp(pkg.version, VERSION) > 0) {
|
|
603
|
+
notices.push({
|
|
604
|
+
area: 'CLI',
|
|
605
|
+
text: `CLI 有新版本:当前 v${VERSION} → 最新 v${pkg.version}(更新:a2a self-update,或 npm install -g ${NPM_PACKAGE}@latest)`,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// 2) Skills:GitHub raw 最新 VERSION
|
|
611
|
+
const skillsVer = await fetchText(`https://raw.githubusercontent.com/${REPO}/main/skills/a2a/VERSION`);
|
|
612
|
+
if (skillsVer && typeof skillsVer === 'string' && skillsVer.trim()) {
|
|
613
|
+
notices.push({
|
|
614
|
+
area: 'Skills',
|
|
615
|
+
text: `Skills 最新版本:v${skillsVer.trim()}(更新:git pull 仓库后重新拷贝 skills/a2a/ 到对应位置,见 INSTALL.md)`,
|
|
616
|
+
});
|
|
617
|
+
} else {
|
|
618
|
+
const skillsJson = await fetchJson(`https://raw.githubusercontent.com/${REPO}/main/skills/a2a/package.json`);
|
|
619
|
+
if (skillsJson && skillsJson.version) {
|
|
620
|
+
notices.push({
|
|
621
|
+
area: 'Skills',
|
|
622
|
+
text: `Skills 最新版本:v${skillsJson.version}(更新:git pull 仓库后重新拷贝 skills/a2a/,见 INSTALL.md)`,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// 3) 平台:当前实例版本(/api/v1/version)vs GitHub 最新(server/package.json)
|
|
628
|
+
if (config && config.url) {
|
|
629
|
+
const cur = await fetchJson(String(config.url).replace(/\/+$/, '') + '/api/v1/version');
|
|
630
|
+
const latest = await fetchJson(`https://raw.githubusercontent.com/${REPO}/main/server/package.json`);
|
|
631
|
+
if (cur && latest && cmp(latest.version, cur.version) > 0) {
|
|
632
|
+
notices.push({
|
|
633
|
+
area: '平台',
|
|
634
|
+
text: `平台有新版本:当前 v${cur.version} → 最新 v${latest.version}(更新:cd <仓库> && git pull && docker compose up -d --build)`,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return notices;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** 输出更新检查结果;全部最新时输出一行确认 */
|
|
643
|
+
function printUpdateResult(notices) {
|
|
644
|
+
console.log('');
|
|
645
|
+
console.log(hl('===== a2a 更新检查 ====='));
|
|
646
|
+
if (!notices.length) {
|
|
647
|
+
console.log(' 全部组件已是最新版本 ✓');
|
|
648
|
+
} else {
|
|
649
|
+
for (const n of notices) {
|
|
650
|
+
console.log(paint(C.yellow, ` [${n.area}] `) + n.text);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
console.log(hl('========================='));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** `a2a update-check`:强制检查 */
|
|
657
|
+
async function cmdUpdateCheck(ctx) {
|
|
658
|
+
const notices = await checkUpdates(ctx.config);
|
|
659
|
+
printUpdateResult(notices);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** `a2a self-update`:更新 CLI 自身(人类确认后执行) */
|
|
663
|
+
async function cmdSelfUpdate() {
|
|
664
|
+
if (!VERSION) {
|
|
665
|
+
console.log(paint(C.yellow, '当前为单文件拷贝安装(无版本信息),请改用 npm 安装:npm install -g ' + NPM_PACKAGE));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
console.log(`正在从 npm 更新 ${NPM_PACKAGE}(当前 v${VERSION})...`);
|
|
669
|
+
try {
|
|
670
|
+
execSync(`npm install -g ${NPM_PACKAGE}@latest`, { stdio: 'inherit' });
|
|
671
|
+
console.log(paint(C.green, 'CLI 更新完成 ✅ 新版本已生效(重新打开终端或运行 a2a help 确认)'));
|
|
672
|
+
} catch (e) {
|
|
673
|
+
console.log(paint(C.red, '更新失败,请手动执行:npm install -g ' + NPM_PACKAGE + '@latest'));
|
|
674
|
+
process.exitCode = 1;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/* ------------------------------------------------------------------------- *
|
|
679
|
+
* Skills 更新(update-skills / update)
|
|
680
|
+
* 从 GitHub 下载最新 skills/a2a/ 到本地安装位置(探测常见位置,支持 --to 与 --yes)
|
|
681
|
+
* ------------------------------------------------------------------------- */
|
|
682
|
+
|
|
683
|
+
const SKILLS_RAW_BASE = (process.env.A2A_SKILLS_URL || '').replace(/\/+$/, '') ||
|
|
684
|
+
'https://raw.githubusercontent.com/BajaXX/Agent2Agent/main/skills/a2a';
|
|
685
|
+
|
|
686
|
+
const SKILL_FILES = [
|
|
687
|
+
'SKILL.md', 'INSTALL.md', 'VERSION',
|
|
688
|
+
'hooks/session-start.sh', 'hooks/session-start.ps1',
|
|
689
|
+
'rules/cursor.mdc',
|
|
690
|
+
];
|
|
691
|
+
|
|
692
|
+
/** 下载单个文件到目标(带超时,避免网络挂起;失败抛明确错误) */
|
|
693
|
+
async function downloadTo(relPath, destDir) {
|
|
694
|
+
const url = `${SKILLS_RAW_BASE}/${relPath}`;
|
|
695
|
+
const ctrl = new AbortController();
|
|
696
|
+
const timer = setTimeout(() => ctrl.abort(), 20000); // 20s 超时
|
|
697
|
+
let res;
|
|
698
|
+
try {
|
|
699
|
+
res = await fetch(url, { headers: { 'User-Agent': 'a2a-cli' }, signal: ctrl.signal });
|
|
700
|
+
} catch (e) {
|
|
701
|
+
clearTimeout(timer);
|
|
702
|
+
throw new Error(`连接超时/失败(${SKILLS_RAW_BASE.replace(/^https?:\/\//, '')}),无法访问 GitHub 下载源;可稍后重试,或设置镜像源:A2A_SKILLS_URL=<可访问的镜像地址>`);
|
|
703
|
+
}
|
|
704
|
+
clearTimeout(timer);
|
|
705
|
+
if (!res.ok) throw new Error(`下载失败 ${relPath} (HTTP ${res.status})`);
|
|
706
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
707
|
+
const dest = path.join(destDir, ...relPath.split('/'));
|
|
708
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
709
|
+
fs.writeFileSync(dest, buf);
|
|
710
|
+
if (relPath.endsWith('.sh')) {
|
|
711
|
+
try { fs.chmodSync(dest, 0o755); } catch (e) { /* Windows 忽略 */ }
|
|
712
|
+
}
|
|
713
|
+
return dest;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** 探测已安装的 skills 位置(存在才返回) */
|
|
717
|
+
function detectSkillLocations() {
|
|
718
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
719
|
+
const found = [];
|
|
720
|
+
const claudeDir = path.join(home, '.claude', 'skills', 'a2a');
|
|
721
|
+
if (fs.existsSync(claudeDir)) found.push({ type: 'claude', dir: claudeDir, label: 'Claude Code skill' });
|
|
722
|
+
const cursorRules = path.join(home, '.cursor', 'rules');
|
|
723
|
+
if (fs.existsSync(cursorRules)) {
|
|
724
|
+
if (fs.existsSync(path.join(cursorRules, 'a2a.mdc')) || fs.existsSync(path.join(cursorRules, 'agent-platform.mdc')) || fs.existsSync(path.join(cursorRules, 'cursor.mdc'))) {
|
|
725
|
+
found.push({ type: 'cursor', dir: cursorRules, label: 'Cursor 规则' });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const windsurfRules = path.join(home, '.windsurf', 'rules');
|
|
729
|
+
if (fs.existsSync(path.join(windsurfRules, 'a2a.md'))) {
|
|
730
|
+
found.push({ type: 'windsurf', dir: windsurfRules, label: 'Windsurf 规则' });
|
|
731
|
+
}
|
|
732
|
+
return found;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** `a2a update-skills`:更新已安装的 skills(Claude/…/--to 指定目录) */
|
|
736
|
+
async function cmdUpdateSkills(opts) {
|
|
737
|
+
const autoYes = Boolean(opts.yes || opts['yes'] || opts.y);
|
|
738
|
+
const targets = [];
|
|
739
|
+
|
|
740
|
+
if (opts.to) {
|
|
741
|
+
const abs = path.resolve(process.cwd(), String(opts.to));
|
|
742
|
+
targets.push({ type: 'dir', dir: abs, label: `指定目录 ${abs}` });
|
|
743
|
+
} else {
|
|
744
|
+
targets.push(...detectSkillLocations());
|
|
745
|
+
if (!targets.length) {
|
|
746
|
+
console.log(paint(C.yellow, '未检测到已安装的 skills。可用:'));
|
|
747
|
+
console.log(' a2a update-skills --to <目录> # 下载整个技能包到指定目录(如 ~/.claude/skills/a2a)');
|
|
748
|
+
console.log(' (Claude Code 安装到 ~/.claude/skills/a2a 后,之后可直接 a2a update-skills)');
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
console.log(hl('===== a2a update-skills ====='));
|
|
754
|
+
for (const t of targets) console.log(` 将更新: [${t.label}]`);
|
|
755
|
+
|
|
756
|
+
if (!autoYes && process.stdin.isTTY) {
|
|
757
|
+
console.log('');
|
|
758
|
+
process.stdout.write(paint(C.dim, '确认更新?(y/N,30 秒无输入自动取消) '));
|
|
759
|
+
const nextLine = createLineReader();
|
|
760
|
+
const ans = await Promise.race([
|
|
761
|
+
nextLine(),
|
|
762
|
+
new Promise((r) => setTimeout(() => r(''), 30000)),
|
|
763
|
+
]);
|
|
764
|
+
if (!/^y/i.test(String(ans || ''))) {
|
|
765
|
+
console.log(paint(C.yellow, '已取消(可用 a2a update-skills --yes 跳过确认)'));
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
} else if (!autoYes && !process.stdin.isTTY) {
|
|
769
|
+
console.log(paint(C.yellow, '(非交互环境:加 --yes 跳过确认)'));
|
|
770
|
+
if (!process.env.A2A_AUTO_UPDATE) return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
console.log(paint(C.dim, `下载源: ${SKILLS_RAW_BASE}(网络较慢时可用 A2A_SKILLS_URL 指定镜像)`));
|
|
774
|
+
let okCount = 0;
|
|
775
|
+
for (const t of targets) {
|
|
776
|
+
try {
|
|
777
|
+
if (t.type === 'cursor') {
|
|
778
|
+
// 规则目录:写入 a2a.mdc,删除旧文件名
|
|
779
|
+
process.stdout.write(` ⏳ ${t.label} 下载中...`);
|
|
780
|
+
const dest = await downloadTo('rules/cursor.mdc', t.dir);
|
|
781
|
+
const renamed = path.join(t.dir, 'a2a.mdc');
|
|
782
|
+
fs.renameSync(dest, renamed);
|
|
783
|
+
for (const old of ['agent-platform.mdc', 'cursor.mdc']) {
|
|
784
|
+
const p = path.join(t.dir, old);
|
|
785
|
+
if (fs.existsSync(p)) { try { fs.unlinkSync(p); } catch (e) { /* ignore */ } }
|
|
786
|
+
}
|
|
787
|
+
console.log(`\r ✓ ${t.label} → ${renamed}`);
|
|
788
|
+
} else {
|
|
789
|
+
process.stdout.write(` ⏳ ${t.label} 下载中...`);
|
|
790
|
+
for (const rel of SKILL_FILES) await downloadTo(rel, t.dir);
|
|
791
|
+
console.log(`\r ✓ ${t.label} → ${t.dir}(${SKILL_FILES.length} 个文件)`);
|
|
792
|
+
}
|
|
793
|
+
okCount++;
|
|
794
|
+
} catch (e) {
|
|
795
|
+
console.log(`\r ✗ ${t.label} 更新失败: ${paint(C.red, e.message)}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
console.log(hl(`更新完成(成功 ${okCount}/${targets.length})`));
|
|
799
|
+
if (okCount < targets.length) process.exitCode = 1;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** `a2a update`:一键更新 CLI + Skills(平台由运维执行 docker 命令) */
|
|
803
|
+
async function cmdUpdate(opts) {
|
|
804
|
+
console.log(hl('===== a2a update ====='));
|
|
805
|
+
if (VERSION) {
|
|
806
|
+
// 先检查 CLI 版本
|
|
807
|
+
const pkg = await fetchJson('https://registry.npmjs.org/' + NPM_PACKAGE + '/latest');
|
|
808
|
+
if (pkg && pkg.version && VERSION !== pkg.version) {
|
|
809
|
+
console.log(`[CLI] 当前 v${VERSION} → 最新 v${pkg.version},正在更新...`);
|
|
810
|
+
try {
|
|
811
|
+
execSync(`npm install -g ${NPM_PACKAGE}@latest`, { stdio: 'inherit' });
|
|
812
|
+
console.log(paint(C.green, '[CLI] 更新完成 ✅'));
|
|
813
|
+
} catch (e) {
|
|
814
|
+
console.log(paint(C.red, `[CLI] 更新失败,请手动:npm install -g ${NPM_PACKAGE}@latest`));
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
console.log(`[CLI] 已是最新 v${VERSION} ✓`);
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
console.log(paint(C.yellow, '[CLI] 单文件安装无版本信息,建议:npm install -g ' + NPM_PACKAGE));
|
|
821
|
+
}
|
|
822
|
+
await cmdUpdateSkills({ yes: opts.yes || opts.y });
|
|
823
|
+
console.log('');
|
|
824
|
+
console.log('平台更新(在部署服务器执行):cd <仓库> && git pull && docker compose up -d --build');
|
|
825
|
+
}
|
|
826
|
+
|
|
513
827
|
/* ------------------------------------------------------------------------- *
|
|
514
828
|
* 命令实现
|
|
515
829
|
* ------------------------------------------------------------------------- */
|
|
@@ -706,32 +1020,68 @@ async function cmdCheckin(opts, ctx) {
|
|
|
706
1020
|
console.log(hl('========== a2a checkin =========='));
|
|
707
1021
|
console.log(`账号: ${acct.name || acct.id || ctx.config.accountId} 状态: ${acct.status || 'starting'}`);
|
|
708
1022
|
console.log(`记忆版本: v${mem.version ?? 0}`);
|
|
709
|
-
|
|
1023
|
+
// 记忆维护提示:空记忆 / 版本过低时提醒 agent 写回(记忆由 agent 自己维护)
|
|
1024
|
+
const memEmpty = !mem.content || !String(mem.content || '').trim();
|
|
1025
|
+
if (memEmpty) {
|
|
1026
|
+
console.log(paint(C.yellow, ' ⚠ 记忆为空:本账号尚无 memory.md。请在会话中/结束时把「进展、决策、待办、协作关系」整理成记忆文件,用 a2a memory set <file> 写回(跨会话保持上下文的关键)。'));
|
|
1027
|
+
} else if ((mem.version || 0) < 2) {
|
|
1028
|
+
console.log(paint(C.dim, ' (提示:建议每次会话结束前用 a2a memory set 更新记忆,保持 v' + (mem.version || 0) + ' → 演进)'));
|
|
1029
|
+
}
|
|
1030
|
+
// 待我回复的消息(发给我的、needsReply、未 resolved)—— 别人等待我回复
|
|
1031
|
+
const needMyReply = inboxItems.filter((m) => m.needsReply && m.status !== 'resolved');
|
|
1032
|
+
const unreadNow = (pending.unreadMessages ?? 0);
|
|
1033
|
+
console.log(`未读消息: ${unreadNow} 条 待你回复: ${needMyReply.length} 条 待办任务: ${pending.todoTasks ?? taskItems.length} 个`);
|
|
710
1034
|
|
|
711
1035
|
console.log('');
|
|
712
|
-
console.log(paint(C.bold, '
|
|
1036
|
+
console.log(paint(C.bold, '收件箱消息:'));
|
|
713
1037
|
if (inboxItems.length === 0) {
|
|
714
|
-
console.log('
|
|
1038
|
+
console.log(' (无新消息)');
|
|
715
1039
|
} else {
|
|
716
|
-
inboxItems.forEach((m, i) =>
|
|
1040
|
+
inboxItems.forEach((m, i) => {
|
|
1041
|
+
const needReply = m.needsReply && m.status !== 'resolved' ? ' ' + paint(C.yellow, '[需你回复]') : '';
|
|
1042
|
+
console.log(` [${i + 1}] ${m.subject || '(无主题)'} — 来自 ${m.from || '?'}${needReply}`);
|
|
1043
|
+
});
|
|
717
1044
|
}
|
|
718
1045
|
|
|
719
1046
|
console.log('');
|
|
720
|
-
console.log(paint(C.bold, '
|
|
721
|
-
|
|
1047
|
+
console.log(paint(C.bold, '我的任务(todo / doing / blocked):'));
|
|
1048
|
+
const myTasks = toArray(await api(config, 'GET', '/tasks', { query: { account: ctx.config.accountId } }))
|
|
1049
|
+
.filter((t) => t.status !== 'done');
|
|
1050
|
+
if (myTasks.length === 0) {
|
|
722
1051
|
console.log(' (无)');
|
|
723
1052
|
} else {
|
|
724
|
-
|
|
1053
|
+
myTasks.forEach((t, i) => {
|
|
1054
|
+
const stayH = t.updatedAt ? Math.floor((Date.now() - t.updatedAt) / 3600000) : 0;
|
|
1055
|
+
const stay = stayH > 24
|
|
1056
|
+
? ' ' + paint(C.yellow, `(滞留 ${Math.floor(stayH / 24)}d${stayH % 24}h:若等待他人/人类介入请 a2a task update 标 blocked 并说明原因)`)
|
|
1057
|
+
: (stayH > 4 ? `(已 ${stayH}h)` : '');
|
|
1058
|
+
console.log(` [${i + 1}] ${t.title || '(无标题)'}(${t.status || '?'})${t.assigneeId ? '→ ' + t.assigneeId : ''}${stay}`);
|
|
1059
|
+
});
|
|
725
1060
|
}
|
|
726
1061
|
|
|
727
1062
|
console.log('');
|
|
728
|
-
if (
|
|
729
|
-
console.log(paint(C.yellow,
|
|
1063
|
+
if (needMyReply.length > 0) {
|
|
1064
|
+
console.log(paint(C.yellow, `→ 有 ${needMyReply.length} 条消息等待你回复:用 a2a inbox 查看后 a2a reply --msg ID --body "...",处理完 a2a mark --msg ID --status resolved`));
|
|
730
1065
|
}
|
|
731
|
-
if ((pending.todoTasks ??
|
|
732
|
-
console.log(paint(C.yellow, '→
|
|
1066
|
+
if ((pending.todoTasks ?? 0) > 0 || myTasks.length > 0) {
|
|
1067
|
+
console.log(paint(C.yellow, '→ 推进任务:a2a task list 查看 → 完成 a2a task update --id ID --status done --note 说明'));
|
|
733
1068
|
}
|
|
734
1069
|
console.log(hl('======================================='));
|
|
1070
|
+
|
|
1071
|
+
// 更新检查(≤24h 一次;网络不可达静默跳过;有更新才提示)
|
|
1072
|
+
const lastCheck = state.lastUpdateCheckAt || 0;
|
|
1073
|
+
if (Date.now() - lastCheck > 24 * 3600 * 1000) {
|
|
1074
|
+
try {
|
|
1075
|
+
const notices = await checkUpdates(config);
|
|
1076
|
+
state.lastUpdateCheckAt = Date.now();
|
|
1077
|
+
saveState(dir, state);
|
|
1078
|
+
if (notices.length) {
|
|
1079
|
+
console.log('');
|
|
1080
|
+
console.log(paint(C.yellow, '→ 检测到可用更新(运行 a2a update-check 查看详情):'));
|
|
1081
|
+
for (const n of notices) console.log(` [${n.area}] ${n.text}`);
|
|
1082
|
+
}
|
|
1083
|
+
} catch (e) { /* 静默 */ }
|
|
1084
|
+
}
|
|
735
1085
|
}
|
|
736
1086
|
|
|
737
1087
|
async function cmdSend(opts, ctx) {
|
|
@@ -763,10 +1113,17 @@ async function cmdInbox(opts, ctx, dir) {
|
|
|
763
1113
|
console.log('(无消息)');
|
|
764
1114
|
return;
|
|
765
1115
|
}
|
|
766
|
-
|
|
1116
|
+
// 方向正确的提醒标记:in = 发给我的(needsReply 未解决 → 需你回复);out = 我发出的(未解决 → 等待对方回复)
|
|
1117
|
+
const headers = dir === 'in'
|
|
1118
|
+
? ['ID', '编号', '来自', '主题', '状态', '提醒', '时间']
|
|
1119
|
+
: ['ID', '编号', '发给', '主题', '状态', '提醒', '时间'];
|
|
767
1120
|
const rows = items.map((m, i) => {
|
|
768
1121
|
const peer = dir === 'in' ? m.from : m.to;
|
|
769
|
-
|
|
1122
|
+
let flag = '';
|
|
1123
|
+
if (m.needsReply && m.status !== 'resolved') {
|
|
1124
|
+
flag = dir === 'in' ? paint(C.yellow, '需你回复') : paint(C.dim, '等待回复');
|
|
1125
|
+
}
|
|
1126
|
+
return [m.id || '-', String(i + 1), peer || '-', m.subject || '-', m.status || '-', flag || '-', fmtTime(m.createdAt)];
|
|
770
1127
|
});
|
|
771
1128
|
console.log(renderTable(headers, rows));
|
|
772
1129
|
}
|
|
@@ -904,6 +1261,49 @@ async function cmdDocGet(opts, ctx, pos) {
|
|
|
904
1261
|
console.log(`已保存到 ${outAbs}(${fmtSize(buf.length)})`);
|
|
905
1262
|
}
|
|
906
1263
|
|
|
1264
|
+
const TEXT_EXT_SET = new Set(['md', 'txt', 'json', 'js', 'mjs', 'cjs', 'ts', 'py', 'yaml', 'yml', 'html', 'css', 'xml', 'csv', 'log', 'ini', 'conf', 'sh', 'sql', 'toml']);
|
|
1265
|
+
function isLikelyText(mime, name) {
|
|
1266
|
+
if (typeof mime === 'string' && mime.startsWith('text/')) return true;
|
|
1267
|
+
const ext = String(name || '').split('.').pop().toLowerCase();
|
|
1268
|
+
return TEXT_EXT_SET.has(ext);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* `a2a doc view @账号/路径/文件.md` —— 按 @引用 查看公开文档(只读,他人文档也可看)。
|
|
1273
|
+
* 引用格式:@<accountId>/<doc目录相对路径>,如 @B项目开发/docs/api.md 或 @dsh-预研/A项目需求.md。
|
|
1274
|
+
*/
|
|
1275
|
+
async function cmdDocView(ctx, pos) {
|
|
1276
|
+
const ref = pos[0];
|
|
1277
|
+
if (!ref) fail('doc view 需要 <@账号/路径/文件>,如 a2a doc view @B项目开发/docs/api.md');
|
|
1278
|
+
const clean = String(ref).replace(/^@/, '');
|
|
1279
|
+
const parts = clean.split('/');
|
|
1280
|
+
if (parts.length < 2) fail('引用格式:@账号/路径/文件(至少 @账号/文件)');
|
|
1281
|
+
const account = parts.shift();
|
|
1282
|
+
const name = parts.join('/');
|
|
1283
|
+
if (!account || !name) fail('引用格式:@账号/路径/文件');
|
|
1284
|
+
|
|
1285
|
+
const listRes = await api(ctx.config, 'GET', '/documents', { query: { account, name } });
|
|
1286
|
+
const list = toArray(listRes);
|
|
1287
|
+
if (!list.length) fail(`未找到文档 @${account}/${name}(该账号未上传此文档,或已被删除)`);
|
|
1288
|
+
const doc = list[0];
|
|
1289
|
+
|
|
1290
|
+
const dl = await api(ctx.config, 'GET', `/documents/${doc.id}/content`, { raw: true, query: { inline: 1 } });
|
|
1291
|
+
const buf = dl.buf;
|
|
1292
|
+
|
|
1293
|
+
console.log(`===== @${account}/${doc.name}(${fmtSize(doc.size)},${doc.description || '无描述'})=====`);
|
|
1294
|
+
if (isLikelyText(doc.mime, doc.name)) {
|
|
1295
|
+
const text = buf.toString('utf8');
|
|
1296
|
+
process.stdout.write(text);
|
|
1297
|
+
if (text && !text.endsWith('\n')) process.stdout.write('\n');
|
|
1298
|
+
} else {
|
|
1299
|
+
const outAbs = path.resolve(process.cwd(), doc.name.split('/').pop() || 'doc.bin');
|
|
1300
|
+
fs.mkdirSync(path.dirname(outAbs), { recursive: true });
|
|
1301
|
+
fs.writeFileSync(outAbs, buf);
|
|
1302
|
+
console.log(`(二进制/非文本文件,已保存到 ${outAbs})`);
|
|
1303
|
+
}
|
|
1304
|
+
console.log('==========================================');
|
|
1305
|
+
}
|
|
1306
|
+
|
|
907
1307
|
async function cmdMemoryGet(ctx) {
|
|
908
1308
|
const res = await api(ctx.config, 'GET', '/memory');
|
|
909
1309
|
const version = res.version ?? 0;
|
|
@@ -974,6 +1374,10 @@ function printHelp() {
|
|
|
974
1374
|
['sync', '双向镜像同步本地 doc 目录 ↔ 平台'],
|
|
975
1375
|
['memory', '记忆(get / set)'],
|
|
976
1376
|
['heartbeat', '心跳'],
|
|
1377
|
+
['update-check', '检查各组件是否有新版本'],
|
|
1378
|
+
['update', '一键更新 CLI + skills'],
|
|
1379
|
+
['update-skills', '更新已安装的 skills(--to 指定目录 / --yes 免确认)'],
|
|
1380
|
+
['self-update', '更新 CLI 自身(npm)'],
|
|
977
1381
|
['help', '显示本帮助'],
|
|
978
1382
|
];
|
|
979
1383
|
for (const [c, d] of cmds) console.log(` ${paint(C.cyan, c.padEnd(10))} ${d}`);
|
|
@@ -997,12 +1401,14 @@ function printHelp() {
|
|
|
997
1401
|
console.log(' a2a task list [--status S] [--account A]');
|
|
998
1402
|
console.log(' a2a task update --id <ID> [--status S] [--note N] [--assignee A]');
|
|
999
1403
|
console.log(' a2a doc up <file> [--desc D]');
|
|
1404
|
+
console.log(' a2a doc view <@账号/路径/文件> # 按 @引用 查看文档(只读)');
|
|
1000
1405
|
console.log(' a2a doc ls [--account A]');
|
|
1001
1406
|
console.log(' a2a doc get <id> [--out FILE] [--inline]');
|
|
1002
1407
|
console.log(' a2a sync');
|
|
1003
1408
|
console.log(' a2a memory get');
|
|
1004
1409
|
console.log(' a2a memory set <file>');
|
|
1005
1410
|
console.log(' a2a heartbeat [--status S] [--note N]');
|
|
1411
|
+
console.log(' a2a update-check / a2a self-update # 检查更新 / 更新 CLI 自身');
|
|
1006
1412
|
|
|
1007
1413
|
console.log('');
|
|
1008
1414
|
console.log(paint(C.bold, '示例:'));
|
|
@@ -1015,6 +1421,16 @@ function printHelp() {
|
|
|
1015
1421
|
console.log(' a2a sync');
|
|
1016
1422
|
}
|
|
1017
1423
|
|
|
1424
|
+
/** 打印版本号:a2a version / -v / --version */
|
|
1425
|
+
function printVersion() {
|
|
1426
|
+
if (VERSION) {
|
|
1427
|
+
console.log(`a2a ${VERSION}(${NPM_PACKAGE})`);
|
|
1428
|
+
} else {
|
|
1429
|
+
console.log('a2a 单文件版(无版本信息,建议改用 npm 安装:npm install -g ' + NPM_PACKAGE + ')');
|
|
1430
|
+
}
|
|
1431
|
+
console.log('Agent2Agent 协作平台客户端 · https://github.com/BajaXX/Agent2Agent');
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1018
1434
|
/* ------------------------------------------------------------------------- *
|
|
1019
1435
|
* 入口
|
|
1020
1436
|
* ------------------------------------------------------------------------- */
|
|
@@ -1023,6 +1439,12 @@ async function main() {
|
|
|
1023
1439
|
const { pos, opts } = parseArgs(process.argv.slice(2));
|
|
1024
1440
|
const cmd = pos[0];
|
|
1025
1441
|
|
|
1442
|
+
// 版本号:a2a version / a2a --version / a2a -v
|
|
1443
|
+
if (cmd === 'version' || cmd === '--version' || cmd === '-v' || opts.version) {
|
|
1444
|
+
printVersion();
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1026
1448
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
1027
1449
|
printHelp();
|
|
1028
1450
|
return;
|
|
@@ -1033,7 +1455,32 @@ async function main() {
|
|
|
1033
1455
|
return;
|
|
1034
1456
|
}
|
|
1035
1457
|
|
|
1036
|
-
|
|
1458
|
+
// 更新类命令不依赖项目配置(操作的是本地 CLI / skills)
|
|
1459
|
+
if (cmd === 'self-update') {
|
|
1460
|
+
await cmdSelfUpdate();
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (cmd === 'update-skills') {
|
|
1464
|
+
await cmdUpdateSkills(opts);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (cmd === 'update') {
|
|
1468
|
+
await cmdUpdate(opts);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
let ctx = null;
|
|
1473
|
+
try {
|
|
1474
|
+
ctx = requireConfig(opts.config);
|
|
1475
|
+
} catch (e) {
|
|
1476
|
+
// update-check 允许无配置运行(仅检查 CLI / skills);其余命令必须配置
|
|
1477
|
+
if (cmd === 'update-check') {
|
|
1478
|
+
await cmdUpdateCheck({ config: null });
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
process.stderr.write(String(e.message || e) + '\n');
|
|
1482
|
+
process.exit(1);
|
|
1483
|
+
}
|
|
1037
1484
|
const sub = pos[1];
|
|
1038
1485
|
|
|
1039
1486
|
switch (cmd) {
|
|
@@ -1077,6 +1524,7 @@ async function main() {
|
|
|
1077
1524
|
if (sub === 'up') await cmdDocUp(opts, ctx, pos.slice(2));
|
|
1078
1525
|
else if (sub === 'ls') await cmdDocLs(opts, ctx);
|
|
1079
1526
|
else if (sub === 'get') await cmdDocGet(opts, ctx, pos.slice(2));
|
|
1527
|
+
else if (sub === 'view') await cmdDocView(ctx, pos.slice(2));
|
|
1080
1528
|
else fail('doc 子命令: up | ls | get(用 a2a help 查看用法)');
|
|
1081
1529
|
break;
|
|
1082
1530
|
case 'memory':
|