@bolloon/bolloon-agent 0.1.41 → 0.1.42

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/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { HybridMessenger } from './network/hybrid-messenger.js';
4
4
  import * as ed25519 from '@noble/ed25519';
5
5
  import { sha512 } from '@noble/hashes/sha2.js';
6
6
  import * as fs from 'fs/promises';
7
+ import * as path from 'path';
7
8
  import { documentReader } from './documents/reader.js';
8
9
  import { initMinimax } from './constraints/index.js';
9
10
  import { createAgentSession } from './agents/pi-sdk.js';
@@ -127,33 +128,23 @@ async function bootstrapIdentity() {
127
128
  return { keypair: kp, did, name };
128
129
  }
129
130
  function publishDID(name, kp) {
131
+ // 2026-06-17: 去掉 IPNS 重试机制 — 老逻辑 60s × 10 次 = 10 分钟阻塞,
132
+ // 严重拖慢 agent 启动. 失败就立刻 fallback, 不阻塞主流程.
130
133
  s.step(2, 5, '发布 DID → IPFS (后台)', 'loading');
131
134
  return new Promise((resolve) => {
132
135
  const attempt = async () => {
133
- let lastLogTime = 0;
134
- const retryWithBackoff = async (retries) => {
135
- try {
136
- const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
137
- const result = await auth.registerAgent({ name, services: [] }, kp, '');
138
- s.step(2, 5, '发布 DID → IPFS', 'ok');
139
- resolve({ cid: result.cid });
140
- }
141
- catch (e) {
142
- const now = Date.now();
143
- if (now - lastLogTime > 30000) {
144
- process.stdout.write(` ${YELLOW}⏳ IPNS发布中 (失败${retries}次), 后台重试...${RESET}\r`);
145
- lastLogTime = now;
146
- }
147
- if (retries < 10) {
148
- setTimeout(() => retryWithBackoff(retries + 1), 60000);
149
- }
150
- else {
151
- process.stdout.write(` ${YELLOW}⚠ IPNS发布重试结束,本地模式运行${RESET}\n`);
152
- resolve({});
153
- }
154
- }
155
- };
156
- setTimeout(() => retryWithBackoff(1), 100);
136
+ try {
137
+ const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
138
+ const result = await auth.registerAgent({ name, services: [] }, kp, '');
139
+ s.step(2, 5, '发布 DID → IPFS', 'ok');
140
+ resolve({ cid: result.cid });
141
+ }
142
+ catch (e) {
143
+ // 一次失败直接放弃 — 本地模式运行就够了, 不重试
144
+ process.stdout.write(` ${YELLOW}⚠ IPFS 发布失败 (${e?.message?.slice(0, 80) || 'unknown'}), 本地模式运行${RESET}\n`);
145
+ s.step(2, 5, '发布 DID → IPFS', 'warn');
146
+ resolve({});
147
+ }
157
148
  };
158
149
  attempt();
159
150
  });
@@ -637,6 +628,105 @@ async function runToolCommand(tool, args, outputJson, comm) {
637
628
  response = `消息已发送到 ${peerId.substring(0, 16)}...`;
638
629
  break;
639
630
  }
631
+ // ---- Collaboration: 派任务给对端 agent 跑, 等回结果 ----
632
+ // 走 P2PDirect, 不经 GitHub
633
+ case 'collab': {
634
+ response = '';
635
+ const [peerOrName, ...rest] = args;
636
+ const task = rest.join(' ').trim();
637
+ if (!peerOrName || !task) {
638
+ response = '用法: --collab <peer-name-or-publicKey> "<任务描述>"';
639
+ error = response;
640
+ break;
641
+ }
642
+ let targetPk = peerOrName;
643
+ if (!/^[0-9a-fA-F]{64}$/.test(peerOrName)) {
644
+ const { listPeers } = await import('./network/known-peers.js');
645
+ const peers = await listPeers();
646
+ for (const p of peers) {
647
+ if (p.name === peerOrName) {
648
+ targetPk = p.publicKey;
649
+ break;
650
+ }
651
+ }
652
+ if (targetPk === peerOrName) {
653
+ response = `❌ 找不到 peer "${peerOrName}" (也不是 64-hex publicKey)`;
654
+ error = response;
655
+ break;
656
+ }
657
+ }
658
+ const { resolveIdentity } = await import('./git-transport/chat-repo.js');
659
+ const { P2PDirect } = await import('./network/p2p-direct.js');
660
+ const id = await resolveIdentity();
661
+ const requestId = `collab-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
662
+ // 启动一个 listen 实例专门等 reply (含超时)
663
+ const p2pListen = new P2PDirect({ name: 'cli-collab-listen', role: id.role });
664
+ const replyPromise = new Promise((resolve, reject) => {
665
+ const timer = setTimeout(() => {
666
+ reject(new Error('reply timeout (90s)'));
667
+ }, 90_000);
668
+ p2pListen.on('data', (ev) => {
669
+ try {
670
+ const text = Buffer.isBuffer(ev.data) ? ev.data.toString('utf8') : String(ev.data);
671
+ if (ev.fromPublicKey !== targetPk)
672
+ return; // 不是对方回的就忽略
673
+ const env = JSON.parse(text);
674
+ if (env?.v === 3 && env?.op === 'agent.collab.reply' && env.payload?.requestId === requestId) {
675
+ clearTimeout(timer);
676
+ resolve(env.payload);
677
+ }
678
+ }
679
+ catch { }
680
+ });
681
+ });
682
+ try {
683
+ await p2pListen.start();
684
+ await p2pListen.joinTopic(Buffer.from('bolloon-agent-harness'));
685
+ const envelope = JSON.stringify({
686
+ v: 3,
687
+ op: 'agent.collab.run',
688
+ payload: {
689
+ requestId,
690
+ task,
691
+ fromRole: id.role,
692
+ fromPk: id.publicKey,
693
+ ts: new Date().toISOString(),
694
+ timeoutMs: 85_000,
695
+ },
696
+ });
697
+ const sent = await p2pListen.sendToWithWait(targetPk, Buffer.from(envelope), 8000);
698
+ if (!sent) {
699
+ response = `❌ 握手超时: 对方 ${targetPk.slice(0, 12)}... 不可达`;
700
+ error = response;
701
+ try {
702
+ await p2pListen.stop();
703
+ }
704
+ catch { }
705
+ break;
706
+ }
707
+ process.stdout.write(`⏳ 任务已派给 ${targetPk.slice(0, 12)}..., 等回复 (最多 90s)...\n`);
708
+ const reply = await replyPromise;
709
+ const lines = [
710
+ `✅ 协作完成 (${reply.durationMs ? Math.round(reply.durationMs / 1000) + 's' : '?'})`,
711
+ ` 任务: ${task.slice(0, 80)}${task.length > 80 ? '...' : ''}`,
712
+ ``,
713
+ `📥 对方结果:`,
714
+ `${reply.result || '(empty)'}`,
715
+ ];
716
+ response = lines.join('\n');
717
+ }
718
+ catch (e) {
719
+ response = `❌ 协作失败: ${e?.message ?? e}`;
720
+ error = response;
721
+ }
722
+ finally {
723
+ try {
724
+ await p2pListen.stop();
725
+ }
726
+ catch { }
727
+ }
728
+ break;
729
+ }
640
730
  case 'prompt': {
641
731
  const [text] = args;
642
732
  if (!text) {
@@ -661,6 +751,14 @@ async function runToolCommand(tool, args, outputJson, comm) {
661
751
  `已加载 Harness Skills: ${harnessSkills.length}\n\n` +
662
752
  `Skills:\n${skills.map(s => ` - ${s.name}: ${s.description}`).join('\n')}\n\n` +
663
753
  `Gates: 0-8 (8-Gate 工作流)`;
754
+ // Fix 1: write-back 初始化摘要到 AGENTS.md
755
+ try {
756
+ const fs_write = await import('fs');
757
+ const agentMd = path.join(process.cwd(), 'AGENTS.md');
758
+ const entry = `\n<!-- bolloon-init -->\n**Bollharness 初始化**: ${new Date().toISOString().slice(0, 10)} | Skills: ${skills.length} | Gates: 0-8\n`;
759
+ fs_write.appendFileSync(agentMd, entry);
760
+ }
761
+ catch { /* 静默 */ }
664
762
  break;
665
763
  }
666
764
  case 'harness-gate': {
@@ -717,6 +815,14 @@ async function runToolCommand(tool, args, outputJson, comm) {
717
815
  response = `📊 变更分类: ${result.classification}\n` +
718
816
  `最小路径: ${result.minimum_gates}\n` +
719
817
  `快速通道: ${result.fast_track ? '✅ 可用' : '❌ 不可用'}`;
818
+ // Fix 1: write-back 分类结果到 CLAUDE.md
819
+ try {
820
+ const fs_write = await import('fs');
821
+ const agentMd = path.join(process.cwd(), 'CLAUDE.md');
822
+ const entry = `\n<!-- bolloon-classify -->\n**变更分类**: ${result.classification} | ${description} | ${new Date().toISOString().slice(0, 10)}\n`;
823
+ fs_write.appendFileSync(agentMd, entry);
824
+ }
825
+ catch { /* 静默 */ }
720
826
  break;
721
827
  }
722
828
  case 'harness-context': {
@@ -926,6 +1032,273 @@ async function runToolCommand(tool, args, outputJson, comm) {
926
1032
  }
927
1033
  break;
928
1034
  }
1035
+ // ---- chat transport (commits-as-messages) ----
1036
+ case 'chat-init': {
1037
+ const { chatInit } = await import('./git-transport/chat-repo.js');
1038
+ const r = await chatInit(process.cwd());
1039
+ response = ['✅ chat-init', ...r.messages].join('\n');
1040
+ break;
1041
+ }
1042
+ case 'chat-send': {
1043
+ const { chatSend, resolveIdentity } = await import('./git-transport/chat-repo.js');
1044
+ // body 优先: 显式参数 > stdin
1045
+ let body = args.join(' ').trim();
1046
+ if (!body && !process.stdin.isTTY) {
1047
+ body = await new Promise((resolve) => {
1048
+ let chunks = '';
1049
+ process.stdin.setEncoding('utf8');
1050
+ process.stdin.on('data', (c) => { chunks += c; });
1051
+ process.stdin.on('end', () => resolve(chunks.trim()));
1052
+ process.stdin.on('error', () => resolve(''));
1053
+ // 1s timeout 防止无 stdin 时挂住
1054
+ setTimeout(() => resolve(chunks.trim()), 1000);
1055
+ });
1056
+ }
1057
+ const r = await chatSend({ repoDir: process.cwd(), body });
1058
+ if (!r.ok) {
1059
+ response = `❌ chat-send: ${r.reason}`;
1060
+ error = response;
1061
+ }
1062
+ else {
1063
+ const id = await resolveIdentity();
1064
+ const lines = [
1065
+ `✅ chat-send`,
1066
+ ` role: ${id.role}`,
1067
+ ` sha: ${r.sha?.slice(0, 12)}`,
1068
+ ` pushed: ${r.pushed ? 'yes' : 'no (will retry on next send)'}`,
1069
+ ` file: ${r.filePath}`,
1070
+ ` p2pNotify: ${r.p2pNotifyEligible ? 'eligible' : 'skipped (>4 KiB)'}`,
1071
+ ];
1072
+ response = lines.join('\n');
1073
+ // 短消息且 P2P 在线 → 走 v3 RPC 推通知 (best-effort)
1074
+ if (r.p2pNotifyEligible && r.sha) {
1075
+ try {
1076
+ const { listPeers } = await import('./network/known-peers.js');
1077
+ const peers = listPeers();
1078
+ const peerPks = Object.values(peers).map((p) => p.publicKey);
1079
+ if (peerPks.length > 0 && comm && typeof comm.sendTo === 'function') {
1080
+ const envelope = JSON.stringify({
1081
+ v: 3,
1082
+ op: 'agent.chat.gitnotify',
1083
+ payload: { sha: r.sha, fromPk: id.publicKey, role: id.role, ts: new Date().toISOString(), file: r.filePath },
1084
+ });
1085
+ let pushed = 0;
1086
+ for (const pk of peerPks) {
1087
+ try {
1088
+ comm.sendTo(pk, envelope);
1089
+ pushed++;
1090
+ }
1091
+ catch { }
1092
+ }
1093
+ response += `\n p2p: sent to ${pushed}/${peerPks.length} peer(s)`;
1094
+ }
1095
+ else {
1096
+ response += `\n p2p: no peers or no sendTo`;
1097
+ }
1098
+ }
1099
+ catch (e) {
1100
+ response += `\n p2p: notify failed (${e?.message ?? e})`;
1101
+ }
1102
+ }
1103
+ }
1104
+ break;
1105
+ }
1106
+ case 'chat-pull': {
1107
+ const { chatPull } = await import('./git-transport/chat-repo.js');
1108
+ const { renderOneLine } = await import('./git-transport/chat-render.js');
1109
+ const r = await chatPull({ repoDir: process.cwd() });
1110
+ if (!r.ok) {
1111
+ response = `❌ chat-pull: ${r.reason}`;
1112
+ error = response;
1113
+ }
1114
+ else if (r.newMessages.length === 0) {
1115
+ response = `✅ chat-pull: 0 new (${r.newCommits} commit(s) scanned)`;
1116
+ }
1117
+ else {
1118
+ response = [
1119
+ `✅ chat-pull: ${r.newMessages.length} new message(s)`,
1120
+ ...r.newMessages.map(renderOneLine),
1121
+ ].join('\n');
1122
+ }
1123
+ break;
1124
+ }
1125
+ case 'chat-list': {
1126
+ const { listMessages } = await import('./git-transport/chat-render.js');
1127
+ const limit = (() => {
1128
+ const idx = args.indexOf('--limit');
1129
+ if (idx >= 0 && args[idx + 1])
1130
+ return parseInt(args[idx + 1], 10);
1131
+ return 20;
1132
+ })();
1133
+ const withIdx = args.indexOf('--with');
1134
+ const withRole = withIdx >= 0 ? args[withIdx + 1] : undefined;
1135
+ const all = listMessages(process.cwd(), withRole, limit);
1136
+ const { renderOneLine } = await import('./git-transport/chat-render.js');
1137
+ if (all.length === 0) {
1138
+ response = '(no messages yet — try `bolloon --chat-init` first)';
1139
+ }
1140
+ else {
1141
+ response = [
1142
+ `📜 ${all.length} message(s)${withRole ? ` (with=${withRole})` : ''}:`,
1143
+ ...all.map(renderOneLine),
1144
+ ].join('\n');
1145
+ }
1146
+ break;
1147
+ }
1148
+ case 'chat-watch': {
1149
+ // 长循环, 直接 runToolCommand 内部跑, main 末尾的 process.exit(0) 不会触发
1150
+ // (见 main() 特判)
1151
+ const { chatWatch } = await import('./git-transport/chat-watch.js');
1152
+ const idx = args.indexOf('--interval');
1153
+ const intervalMs = idx >= 0 && args[idx + 1] ? parseInt(args[idx + 1], 10) : undefined;
1154
+ await chatWatch({ repoDir: process.cwd(), intervalMs });
1155
+ response = '✅ chat-watch stopped';
1156
+ break;
1157
+ }
1158
+ case 'chat-status': {
1159
+ const { chatStatus } = await import('./git-transport/chat-repo.js');
1160
+ const s = await chatStatus({ repoDir: process.cwd() });
1161
+ const lines = [
1162
+ `📡 chat status`,
1163
+ ` role: ${s.role}`,
1164
+ ` publicKey: ${s.publicKey.slice(0, 16)}...`,
1165
+ ` repo: ${s.repoDir}`,
1166
+ ` remote: ${s.remote ?? '(none — local-only mode)'}`,
1167
+ ` branch: ${s.branch ?? '(unknown)'}`,
1168
+ ` head: ${s.head ?? '(no commits)'}`,
1169
+ ` ahead/behind: ${s.ahead ?? 0} / ${s.behind ?? 0}`,
1170
+ ` mode: ${s.mode}`,
1171
+ ` files: ${s.fileCount} (${Object.entries(s.byRole).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'})`,
1172
+ ];
1173
+ response = lines.join('\n');
1174
+ break;
1175
+ }
1176
+ // ---- P2P-only chat (no git, no GitHub) ----
1177
+ case 'chat-p2p-send': {
1178
+ response = '';
1179
+ // 形态: --chat-p2p-send <peerOrName> "消息正文"
1180
+ // peerOrName: 64-hex publicKey 或 known_peers.json 里的 name
1181
+ // 走 P2PDirect (纯 TS, 不走坏了的 @diap/sdk HyperswarmCommunicator)
1182
+ const [peerOrName, ...rest] = args;
1183
+ const body = rest.join(' ').trim();
1184
+ if (!peerOrName || !body) {
1185
+ response = '用法: --chat-p2p-send <peer-name-or-publicKey> "消息正文"';
1186
+ error = response;
1187
+ break;
1188
+ }
1189
+ let targetPk = peerOrName;
1190
+ if (!/^[0-9a-fA-F]{64}$/.test(peerOrName)) {
1191
+ const { listPeers } = await import('./network/known-peers.js');
1192
+ const peers = await listPeers();
1193
+ for (const p of peers) {
1194
+ if (p.name === peerOrName) {
1195
+ targetPk = p.publicKey;
1196
+ break;
1197
+ }
1198
+ }
1199
+ if (targetPk === peerOrName) {
1200
+ response = `❌ 找不到 peer "${peerOrName}" (也不是 64-hex publicKey)`;
1201
+ error = response;
1202
+ break;
1203
+ }
1204
+ }
1205
+ const { resolveIdentity } = await import('./git-transport/chat-repo.js');
1206
+ const { P2PDirect } = await import('./network/p2p-direct.js');
1207
+ const id = await resolveIdentity();
1208
+ const envelope = JSON.stringify({
1209
+ v: 3,
1210
+ op: 'agent.chat.direct',
1211
+ payload: {
1212
+ text: body,
1213
+ fromRole: id.role,
1214
+ fromPk: id.publicKey,
1215
+ ts: new Date().toISOString(),
1216
+ },
1217
+ });
1218
+ const p2p = new P2PDirect({ name: 'cli-send', role: id.role });
1219
+ try {
1220
+ await p2p.start();
1221
+ await p2p.joinTopic(Buffer.from('bolloon-agent-harness'));
1222
+ const sent = await p2p.sendToWithWait(targetPk, Buffer.from(envelope), 8000);
1223
+ if (sent) {
1224
+ response = `✅ 私发 → ${targetPk.slice(0, 12)}...\n role: ${id.role}\n text: ${body.slice(0, 80)}${body.length > 80 ? '...' : ''}\n (P2P-only, 未写入 git / GitHub)`;
1225
+ }
1226
+ else {
1227
+ response = `❌ 握手超时: 对方 ${targetPk.slice(0, 12)}... 未在 8s 内响应 (对方可能离线 / NAT 后 / DHT 还在 bootstrap)`;
1228
+ error = response;
1229
+ }
1230
+ }
1231
+ catch (e) {
1232
+ response = `❌ 发送失败: ${e?.message ?? e}`;
1233
+ error = response;
1234
+ }
1235
+ finally {
1236
+ try {
1237
+ await p2p.stop();
1238
+ }
1239
+ catch { }
1240
+ }
1241
+ break;
1242
+ }
1243
+ case 'chat-p2p-listen': {
1244
+ response = '';
1245
+ // 后台长循环: P2PDirect 监听, 只打印 op=agent.chat.direct 的
1246
+ const { resolveIdentity } = await import('./git-transport/chat-repo.js');
1247
+ const { P2PDirect } = await import('./network/p2p-direct.js');
1248
+ const id = await resolveIdentity();
1249
+ const p2p = new P2PDirect({ name: 'cli-listen', role: id.role });
1250
+ process.stdout.write(`[chat-p2p-listen] role=${id.role} pk=${id.publicKey.slice(0, 12)} listening on bolloon-agent-harness\n`);
1251
+ process.stdout.write(`[chat-p2p-listen] press Ctrl-C to stop\n`);
1252
+ const onData = (ev) => {
1253
+ try {
1254
+ const text = Buffer.isBuffer(ev.data) ? ev.data.toString('utf8') : String(ev.data);
1255
+ try {
1256
+ const env = JSON.parse(text);
1257
+ if (env && env.v === 3 && env.op === 'agent.chat.direct') {
1258
+ const { text: body, fromRole } = env.payload || {};
1259
+ const ts = (env.payload?.ts || new Date().toISOString()).replace('T', ' ').replace(/\.\d+Z$/, '');
1260
+ process.stdout.write(`\n[${ts} ${fromRole || ev.fromPublicKey?.slice(0, 12)} → me] ${body}\n> `);
1261
+ return;
1262
+ }
1263
+ }
1264
+ catch { /* 非 v3 envelope, 当 raw 显示 */ }
1265
+ process.stdout.write(`\n[raw ${ev.fromPublicKey?.slice(0, 12)}] ${text.slice(0, 200)}\n> `);
1266
+ }
1267
+ catch (e) {
1268
+ process.stdout.write(`[chat-p2p-listen] decode error: ${e?.message ?? e}\n`);
1269
+ }
1270
+ };
1271
+ p2p.on('data', onData);
1272
+ let lastPing = 0;
1273
+ const keepAlive = setInterval(() => {
1274
+ const now = Date.now();
1275
+ if (now - lastPing > 5 * 60_000) {
1276
+ process.stdout.write(`[chat-p2p-listen] alive, role=${id.role}\n`);
1277
+ lastPing = now;
1278
+ }
1279
+ }, 30_000);
1280
+ const stop = async () => {
1281
+ process.stdout.write(`\n[chat-p2p-listen] stopping...\n`);
1282
+ try {
1283
+ p2p.off('data', onData);
1284
+ }
1285
+ catch { }
1286
+ clearInterval(keepAlive);
1287
+ try {
1288
+ await p2p.stop();
1289
+ }
1290
+ catch { }
1291
+ process.exit(0);
1292
+ };
1293
+ process.on('SIGINT', stop);
1294
+ process.on('SIGTERM', stop);
1295
+ process.on('SIGHUP', stop);
1296
+ await p2p.start();
1297
+ await p2p.joinTopic(Buffer.from('bolloon-agent-harness'));
1298
+ process.stdout.write(`[chat-p2p-listen] joined topic ✓\n> `);
1299
+ await new Promise(() => { });
1300
+ break;
1301
+ }
929
1302
  default:
930
1303
  response = `错误: 未知工具 "${tool}"`;
931
1304
  error = response;
@@ -1218,6 +1591,61 @@ function parseArgs() {
1218
1591
  }
1219
1592
  result.toolArgs = sessionArgs;
1220
1593
  break;
1594
+ // --- chat transport (commits as messages) ---
1595
+ case '--chat-init':
1596
+ result.chatInit = true;
1597
+ result.tool = 'chat-init';
1598
+ break;
1599
+ case '--chat-send':
1600
+ result.chatSend = true;
1601
+ result.tool = 'chat-send';
1602
+ // 吃掉所有非 flag 参数作为消息体 (--chat-send "消息正文" 或 stdin)
1603
+ const chatSendArgs = [];
1604
+ while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
1605
+ chatSendArgs.push(args[++i]);
1606
+ }
1607
+ result.toolArgs = chatSendArgs;
1608
+ break;
1609
+ case '--chat-pull':
1610
+ result.chatPull = true;
1611
+ result.tool = 'chat-pull';
1612
+ break;
1613
+ case '--chat-list':
1614
+ result.chatList = true;
1615
+ result.tool = 'chat-list';
1616
+ break;
1617
+ case '--chat-watch':
1618
+ result.chatWatch = true;
1619
+ result.tool = 'chat-watch';
1620
+ const watchArgs = [];
1621
+ while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
1622
+ watchArgs.push(args[++i]);
1623
+ }
1624
+ result.toolArgs = watchArgs;
1625
+ break;
1626
+ case '--chat-status':
1627
+ result.chatStatus = true;
1628
+ result.tool = 'chat-status';
1629
+ break;
1630
+ case '--chat-p2p-send':
1631
+ result.tool = 'chat-p2p-send';
1632
+ const p2pSendArgs = [];
1633
+ while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
1634
+ p2pSendArgs.push(args[++i]);
1635
+ }
1636
+ result.toolArgs = p2pSendArgs;
1637
+ break;
1638
+ case '--chat-p2p-listen':
1639
+ result.tool = 'chat-p2p-listen';
1640
+ break;
1641
+ case '--collab':
1642
+ result.tool = 'collab';
1643
+ const collabArgs = [];
1644
+ while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
1645
+ collabArgs.push(args[++i]);
1646
+ }
1647
+ result.toolArgs = collabArgs;
1648
+ break;
1221
1649
  case '--update-check':
1222
1650
  result.updateCheck = true;
1223
1651
  result.tool = 'update-check';
@@ -1304,6 +1732,21 @@ function printHelp() {
1304
1732
  --update-check 检查 npm 包更新
1305
1733
  --update-now [pkg] 更新到最新版本
1306
1734
 
1735
+ # 跨机聊天 (commits-as-messages, 共享 GitHub 仓库)
1736
+ --chat-init 初始化 .comm/ 目录 (一次性)
1737
+ --chat-send "消息正文" 把消息写到 .comm/<role>/, commit + push
1738
+ --chat-pull 拉取远端 .comm/ 的新消息并显示
1739
+ --chat-list 列出本地所有已同步消息
1740
+ --chat-watch [--interval 15s] 后台定时拉取, 有新消息时输出
1741
+ --chat-status 一屏查看: role / publicKey / remote / ahead-behind
1742
+
1743
+ # 纯 P2P 私聊 (不走 GitHub, 不写 git, 不持久化)
1744
+ --chat-p2p-send <peer|publicKey> "消息正文" 通过 P2P 直接发一条
1745
+ --chat-p2p-listen 后台监听对方 P2P 私聊消息 (Ctrl-C 退出)
1746
+
1747
+ # 跨机 agent 协作 (对方 bolloon --web 起着才能处理, 走 P2P 不经 GitHub)
1748
+ --collab <peer|publicKey> "<任务描述>" 派任务给对端 LLM 干活, 等回结果 (90s 超时)
1749
+
1307
1750
  # AI 对话
1308
1751
  --prompt, -p <text> 通用 AI 对话(默认)
1309
1752
  --model <name> 指定使用的模型
@@ -1497,7 +1940,10 @@ async function main() {
1497
1940
  console.log();
1498
1941
  await runNonInteractive(args, comm);
1499
1942
  comm?.stop();
1500
- process.exit(0);
1943
+ // chat-watch / chat-p2p-listen 是长循环, 不会自然 return, 走 SIGINT 自然退出
1944
+ if (!args.chatWatch && args.tool !== 'chat-p2p-listen') {
1945
+ process.exit(0);
1946
+ }
1501
1947
  }
1502
1948
  else {
1503
1949
  s.section('对话模式');