@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/.comm/README.md +21 -0
- package/.comm/default/2026-06-17T08-23-00-017Z-8a735de8.md +7 -0
- package/CLAUDE.md +3 -0
- package/dist/agents/intent-classifier.js +99 -0
- package/dist/agents/pi-sdk.js +1364 -58
- package/dist/agents/pre-tool-validator.js +2 -1
- package/dist/agents/shell-guard.js +9 -3
- package/dist/agents/shell-tool.js +28 -13
- package/dist/agents/task-state.js +224 -0
- package/dist/agents/workflow-pivot-loop.js +30 -0
- package/dist/bollharness-integration/gate-state-machine.js +13 -0
- package/dist/bollharness-integration/integration.js +71 -0
- package/dist/bollharness-integration/skill-adapter.js +52 -127
- package/dist/bootstrap/context-hierarchy.js +6 -4
- package/dist/cli/interface.js +28 -0
- package/dist/documents/reader.js +14 -0
- package/dist/git-transport/chat-render.js +155 -0
- package/dist/git-transport/chat-repo.js +370 -0
- package/dist/git-transport/chat-types.js +23 -0
- package/dist/git-transport/chat-watch.js +102 -0
- package/dist/index.js +471 -25
- package/dist/llm/pi-ai.js +103 -27
- package/dist/network/local-inbox-bus.js +73 -0
- package/dist/network/p2p-direct.js +3 -4
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +6 -2
- package/dist/pi-ecosystem-judgment/causal-judge.js +3 -3
- package/dist/pi-ecosystem-judgment/index.js +1 -1
- package/dist/security/tool-gate.js +11 -0
- package/dist/web/server.js +59 -2
- package/package.json +2 -2
- package/scripts/lefthook-helper.sh +69 -0
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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('对话模式');
|