@bolloon/bolloon-agent 0.3.41 → 0.3.43

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.
@@ -167,6 +167,15 @@ export class PiAgentSession {
167
167
  }
168
168
  })();
169
169
  }
170
+ /** 2026-08-08: 公开可用工具列表 (name + description + 参数名) 供 /tools 显示 */
171
+ getToolList() {
172
+ const out = [];
173
+ for (const tool of this.allowedTools()) {
174
+ const paramNames = tool.parameters ? Object.keys(tool.parameters) : [];
175
+ out.push({ name: tool.name, description: tool.description || '', parameters: paramNames });
176
+ }
177
+ return out;
178
+ }
170
179
  /**
171
180
  * Judgment 注入门临时结果: 在 prompt / promptStream / promptWithPivotLoop 入口算一次, 拼到本轮 systemPrompt 末尾
172
181
  * 每次调用都会重置 (避免上一轮遗留)
@@ -146,8 +146,26 @@ export async function writeSkillCandidate(c) {
146
146
  const dir = getCandidateDir();
147
147
  await fs.mkdir(dir, { recursive: true });
148
148
  const safeName = sanitizeSkillName(c.name);
149
- const file = path.join(dir, `${safeName}-${Date.now()}.json`);
150
- await fs.writeFile(file, JSON.stringify(c, null, 2), 'utf-8');
149
+ // 2026-08-08: signature 的候选用固定文件名 (合并更新同一个), 无 signature 才带时间戳
150
+ const file = c.signature
151
+ ? path.join(dir, `${safeName}.json`)
152
+ : path.join(dir, `${safeName}-${Date.now()}.json`);
153
+ // 追加式合并: 若同 signature 已存在, 累积 runs + 追加 body
154
+ let runs = c.runs ?? 1;
155
+ let body = c.body;
156
+ try {
157
+ const prev = JSON.parse(await fs.readFile(file, 'utf-8'));
158
+ if (prev && prev.runs)
159
+ runs = prev.runs + 1;
160
+ if (prev && prev.body && body !== prev.body && c.signature) {
161
+ // 同一 signature 重复运行 → 追加一条经验 (去重, 避免 body 膨胀)
162
+ const line = `- ${new Date().toISOString().slice(0, 16)} ${c.source}: ${c.description}`;
163
+ body = `${prev.body}\n${line}`;
164
+ }
165
+ }
166
+ catch { /* 新文件 */ }
167
+ const merged = { ...c, runs, body, timestamp: c.timestamp || new Date().toISOString() };
168
+ await fs.writeFile(file, JSON.stringify(merged, null, 2), 'utf-8');
151
169
  return file;
152
170
  }
153
171
  export async function listSkillCandidates(home = os.homedir()) {
@@ -165,12 +183,37 @@ export async function listSkillCandidates(home = os.homedir()) {
165
183
  const raw = await fs.readFile(path.join(dir, f), 'utf-8');
166
184
  const c = JSON.parse(raw);
167
185
  if (c.name && c.body)
168
- out.push(c);
186
+ out.push({ ...c, file: path.join(dir, f) });
169
187
  }
170
188
  catch { /* 坏文件跳过 */ }
171
189
  }
172
190
  return out;
173
191
  }
192
+ /** 按名字删除所有同名候选文件 (名可能与文件名前缀不完全一致) */
193
+ async function removeCandidateFiles(name, home) {
194
+ const safe = sanitizeSkillName(name);
195
+ const dir = getCandidateDir(home);
196
+ let files;
197
+ try {
198
+ files = await fs.readdir(dir);
199
+ }
200
+ catch {
201
+ return;
202
+ }
203
+ for (const f of files) {
204
+ if (!f.endsWith('.json'))
205
+ continue;
206
+ try {
207
+ const c = JSON.parse(await fs.readFile(path.join(dir, f), 'utf-8'));
208
+ if (sanitizeSkillName(c.name) === safe)
209
+ await fs.rm(path.join(dir, f), { force: true });
210
+ }
211
+ catch {
212
+ if (f.startsWith(safe))
213
+ await fs.rm(path.join(dir, f), { force: true });
214
+ }
215
+ }
216
+ }
174
217
  /** 把候选转正为正式 skill (可选: 转正后删除候选文件) */
175
218
  export async function promoteCandidate(name, opts = {}, home = os.homedir()) {
176
219
  const candidates = await listSkillCandidates(home);
@@ -179,18 +222,25 @@ export async function promoteCandidate(name, opts = {}, home = os.homedir()) {
179
222
  return { ok: false, path: '', error: `候选 '${name}' 不存在` };
180
223
  const r = await createSkill(c.name, c.description, c.body, opts);
181
224
  if (r.ok) {
182
- // 清理已转正的候选文件
183
- try {
184
- const dir = getCandidateDir(home);
185
- for (const f of (await fs.readdir(dir))) {
186
- if (f.startsWith(sanitizeSkillName(c.name) + '-'))
187
- await fs.rm(path.join(dir, f), { force: true });
188
- }
189
- }
190
- catch { /* 清理失败不阻塞 */ }
225
+ // 清理已转正的候选文件: 同 name 的所有候选
226
+ await removeCandidateFiles(c.name, home);
191
227
  }
192
228
  return r;
193
229
  }
230
+ /**
231
+ * 从一轮成功的工具调用生成稳定签名 — 同一套工具序列 (有序去重, 最多 4 个) 视为同一经验.
232
+ * 用于跨运行合并: 第二次跑同样的工具 → 更新同一个候选, 而不是新建一个.
233
+ */
234
+ export function toolSignature(okSteps) {
235
+ const seq = [];
236
+ for (const s of (okSteps || [])) {
237
+ if (s.name && !seq.includes(s.name))
238
+ seq.push(s.name);
239
+ if (seq.length >= 4)
240
+ break;
241
+ }
242
+ return seq.join('_');
243
+ }
194
244
  export async function writeRunEndSkillCandidates(steps, source, minOk = 2) {
195
245
  const okSteps = (steps || []).filter((s) => s.status === 'ok' && s.name && s.name !== 'system' && s.name !== '?');
196
246
  if (okSteps.length < minOk) {
@@ -200,13 +250,19 @@ export async function writeRunEndSkillCandidates(steps, source, minOk = 2) {
200
250
  const body = `## 背景\n本轮对话连续成功调用了 ${okSteps.length} 个工具: ${toolNames}.\n\n` +
201
251
  `## 流程\n${okSteps.map((s) => `1. 调用 ${s.name}${s.output ? ': ' + String(s.output).slice(0, 120) : ''}`).join('\n')}\n\n` +
202
252
  `## 注意事项\n- 工具名以 list_skills / get_operation_logs 的实际注册名为准\n- 沉淀为正式 skill 前请人工确认流程可复用\n`;
203
- const candName = `auto-${okSteps[0].name}-${Date.now().toString(36)}`;
253
+ // 2026-08-08: 稳定签名 + 固定文件名 → 同一套工具反复跑时合并更新到同一个候选 (runs++)
254
+ const signature = toolSignature(okSteps);
255
+ const candName = `auto-${signature}`;
256
+ const existing = (await listSkillCandidates()).find((x) => x.signature === signature || sanitizeSkillName(x.name) === sanitizeSkillName(candName));
204
257
  const file = await writeSkillCandidate({
205
258
  name: candName,
206
259
  description: `自动候选: ${okSteps.length} 个工具连续成功 (${toolNames})`,
207
260
  body,
208
261
  source,
209
262
  timestamp: new Date().toISOString(),
263
+ signature,
210
264
  });
211
- return { wrote: true, file, count: okSteps.length, names: toolNames };
265
+ const merged = !!existing;
266
+ const runs = (existing?.runs ?? 0) + 1;
267
+ return { wrote: true, file, count: okSteps.length, names: toolNames, merged, runs };
212
268
  }
@@ -30,14 +30,16 @@ const Messages = ({ msgs }) => (_jsx(Box, { flexDirection: "column", flexGrow: 1
30
30
  }) }));
31
31
  const MentionPopup = ({ title, items, sel, width, loading }) => {
32
32
  const MAX_ROWS = 8;
33
- const shown = items.slice(0, MAX_ROWS);
33
+ // 2026-08-08: 滑动窗口 — 选中项始终可见 (原实现 fix 屏幕顶部, sel 超窗口时无高亮行)
34
+ const offset = Math.max(0, Math.min(sel - Math.floor(MAX_ROWS / 2), Math.max(0, items.length - MAX_ROWS)));
35
+ const shown = items.slice(offset, offset + MAX_ROWS);
34
36
  const innerW = Math.max(width - 2, 10);
35
37
  return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(Text, { color: "cyan", bold: true, children: `╭─ ${title} ${'─'.repeat(Math.max(2, innerW - dispWidth(title) - 4))}╮` }), loading && items.length === 0 ? (_jsx(Text, { color: "dim", children: "\u2502 \u626B\u63CF\u4E2D..." })) : !loading && items.length === 0 ? (_jsx(Text, { color: "dim", children: "\u2502 \u65E0\u5339\u914D" })) : (shown.map((it, i) => {
36
38
  const active = i === sel;
37
39
  const label = it.kind === 'file' ? it.label : `${it.kind === 'skill' ? '⚡' : it.kind === 'plugin' ? '🔌' : ''}${it.label}`;
38
40
  const hint = it.hint ? `${it.hint}` : it.kind === 'file' ? '文件' : '';
39
41
  return (_jsxs(Box, { width: innerW, children: [_jsx(Text, { color: active ? 'black' : undefined, backgroundColor: active ? 'cyan' : undefined, children: `${active ? '❯ ' : ' '}${label}` }), _jsx(Text, { color: active ? 'black' : 'dim', backgroundColor: active ? 'cyan' : undefined, dimColor: !active, children: ` ${hint}` })] }, `${it.kind}:${it.label}`));
40
- })), items.length > MAX_ROWS && (_jsxs(Text, { color: "dim", children: ["\u2502 \u8FD8\u6709 ", items.length - MAX_ROWS, " \u9879..."] })), _jsx(Text, { color: "cyan", children: `╰${'─'.repeat(innerW)}╯` })] }));
42
+ })), items.length > MAX_ROWS && (_jsxs(Text, { color: "dim", children: ["\u2502 ", offset + 1, "-", offset + shown.length, "/", items.length, " \u00B7 \u8FD8\u6709 ", items.length - (offset + shown.length), " \u9879..."] })), _jsx(Text, { color: "cyan", children: `╰${'─'.repeat(innerW)}╯` })] }));
41
43
  };
42
44
  const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH }) => {
43
45
  const [input, setInput] = useState('');
@@ -23,7 +23,7 @@ const CLI_COMMANDS = [
23
23
  { kind: 'command', label: 'add_friend', hint: '添加好友 <64位hex公钥>', insert: 'add_friend' },
24
24
  // 2026-08-06: 系统命令组
25
25
  { kind: 'command', label: 'model', hint: '模型供应商选择器 (↑↓ 选择)', insert: 'model' },
26
- { kind: 'command', label: 'login', hint: '登录模型供应商 (选择器)', insert: 'login' },
26
+ { kind: 'command', label: 'login', hint: '登录 GitHub + Google 账号 (骨架)', insert: 'login' },
27
27
  { kind: 'command', label: 'logout', hint: '查看当前供应商', insert: 'logout' },
28
28
  { kind: 'command', label: 'now', hint: '当前状态总览', insert: 'now' },
29
29
  { kind: 'command', label: 'session', hint: '当前会话信息', insert: 'session' },
@@ -44,14 +44,15 @@ const CLI_COMMANDS = [
44
44
  { kind: 'command', label: 'insight', hint: '洞察 (08-Insights)', insert: 'insight' },
45
45
  { kind: 'command', label: 'wiki', hint: 'wiki 状态', insert: 'wiki' },
46
46
  { kind: 'command', label: 'dream', hint: '随机灵感', insert: 'dream' },
47
+ { kind: 'command', label: 'new agent', hint: '创建新智能体 channel', insert: 'new agent' },
48
+ { kind: 'command', label: 'new session', hint: '创建新会话', insert: 'new session' },
49
+ { kind: 'command', label: 'plan', hint: '循环计划 (创建/查看)', insert: 'plan' },
50
+ { kind: 'command', label: 'todo', hint: '勾选步骤 (循环过程)', insert: 'todo' },
47
51
  ];
48
52
  /** Web 端斜杠命令 (server /message 路由 → LLM 工具) */
49
53
  const WEB_COMMANDS = [
50
- { kind: 'command', label: 'plan', hint: '创建计划', insert: 'plan' },
51
- { kind: 'command', label: 'todo', hint: '勾选步骤', insert: 'todo' },
52
54
  { kind: 'command', label: 'review', hint: '审查计划', insert: 'review' },
53
55
  { kind: 'command', label: 'task', hint: '创建任务', insert: 'task' },
54
- { kind: 'command', label: 'goal', hint: '暂停目标', insert: 'goal' },
55
56
  { kind: 'command', label: 'skill', hint: '沉淀技能', insert: 'skill' },
56
57
  { kind: 'command', label: 'add-friend', hint: '添加好友 (智能体工具)', insert: 'add-friend' },
57
58
  ];
package/dist/index.js CHANGED
@@ -586,6 +586,98 @@ async function processInput(input, comm) {
586
586
  }
587
587
  return;
588
588
  }
589
+ // /new agent <名字> — 创建新智能体 channel (2026-08-08)
590
+ if (trimmed.toLowerCase().startsWith('/new agent')) {
591
+ const q = trimmed.slice('/new agent'.length).trim();
592
+ if (!q) {
593
+ appendLine(`${C_DIM}用法: /new agent <名字> — 新建一个智能体 channel 并切换过去${RESET}`);
594
+ return;
595
+ }
596
+ try {
597
+ const [name, ...rest] = q.split(/\s+/);
598
+ const personaHint = rest.join(' ').trim();
599
+ const { getIdentityStore } = await import('./agents/agent-identity-store.js');
600
+ const store = getIdentityStore();
601
+ await store.load();
602
+ const { readFile, writeFile, mkdir } = await import('fs/promises');
603
+ const { join } = await import('path');
604
+ const home = process.env.HOME || '/tmp';
605
+ const channelsPath = join(home, '.bolloon', 'sessions', 'channels.json');
606
+ let channels = [];
607
+ try {
608
+ const parsed = JSON.parse(await readFile(channelsPath, 'utf-8'));
609
+ channels = Array.isArray(parsed) ? parsed : parsed?.channels || [];
610
+ }
611
+ catch { /* 首次无文件 */ }
612
+ const dupName = channels.find((c) => c.name === name.trim());
613
+ if (dupName) {
614
+ appendLine(`${C_ERROR}同名智能体已存在: '${dupName.name}' (id=${dupName.id})${RESET}`);
615
+ return;
616
+ }
617
+ const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
618
+ const ch = {
619
+ id,
620
+ name: name.trim(),
621
+ agentId: `agent-${name.trim().toLowerCase().replace(/\s+/g, '-')}`,
622
+ createdAt: new Date().toISOString(),
623
+ updatedAt: new Date().toISOString(),
624
+ currentSessionId: 'default',
625
+ };
626
+ if (personaHint)
627
+ ch.persona = { name: name.trim(), description: personaHint };
628
+ channels.push(ch);
629
+ await mkdir(join(home, '.bolloon', 'sessions'), { recursive: true });
630
+ await writeFile(channelsPath, JSON.stringify(channels, null, 2), 'utf-8');
631
+ await store.setActive(id);
632
+ cliAgentName = name.trim();
633
+ cliActiveChannelId = id;
634
+ inkSetStatus(getStatus());
635
+ appendLine(`${C_OK}✓ 已创建智能体 channel: ${name.trim()}${RESET} (${C_DIM}${id}${RESET})${personaHint ? `\n ${C_DIM}persona: ${personaHint}${RESET}` : ''}`);
636
+ }
637
+ catch (e) {
638
+ appendLine(`${C_ERROR}/new agent 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
639
+ }
640
+ return;
641
+ }
642
+ // /new session — 当前 channel 开新会话 (2026-08-08)
643
+ if (trimmed.toLowerCase() === '/new session') {
644
+ try {
645
+ const { readFile, writeFile } = await import('fs/promises');
646
+ const { join } = await import('path');
647
+ const home = process.env.HOME || '/tmp';
648
+ const channelsPath = join(home, '.bolloon', 'sessions', 'channels.json');
649
+ const newSessionId = `sess_${Date.now()}`;
650
+ let saved = false;
651
+ try {
652
+ const parsed = JSON.parse(await readFile(channelsPath, 'utf-8'));
653
+ const channels = Array.isArray(parsed) ? parsed : parsed?.channels || [];
654
+ for (const c of channels) {
655
+ if (cliActiveChannelId && c.id === cliActiveChannelId) {
656
+ c.currentSessionId = newSessionId;
657
+ saved = true;
658
+ }
659
+ else if (!cliActiveChannelId && c.id === channels[0]?.id) {
660
+ c.currentSessionId = newSessionId;
661
+ saved = true;
662
+ }
663
+ }
664
+ await writeFile(channelsPath, JSON.stringify(Array.isArray(parsed) ? channels : { ...parsed, channels }, null, 2), 'utf-8');
665
+ }
666
+ catch { /* 无 channels.json → 仅提示 */ }
667
+ // 重置 agent 消息历史 (新会话空窗口)
668
+ try {
669
+ const a = await getAgent();
670
+ if (a && a.messageHistory)
671
+ a.messageHistory = [];
672
+ }
673
+ catch { /* 非致命 */ }
674
+ appendLine(`${C_OK}✓ 已新建会话${RESET} session=${C_DIM}${newSessionId}${RESET}${saved ? ` (channel: ${cliActiveChannelId || 'default'})` : ''}`);
675
+ }
676
+ catch (e) {
677
+ appendLine(`${C_ERROR}/new session 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
678
+ }
679
+ return;
680
+ }
589
681
  // /queue — 切换队列模式
590
682
  if (trimmed.toLowerCase() === '/queue') {
591
683
  queueMode = !queueMode;
@@ -615,8 +707,8 @@ async function processInput(input, comm) {
615
707
  }
616
708
  // ==================== 2026-08-06: 系统命令组 (/model /now /ipfs /memory ...) ====================
617
709
  const cmd = trimmed.toLowerCase();
618
- // /model /login — 模型供应商选择器 (ink 交互渲染, 复用 MentionPopup)
619
- if (cmd === '/model' || cmd === '/login') {
710
+ // /model — 模型供应商选择器 (ink 交互渲染, 复用 MentionPopup)
711
+ if (cmd === '/model') {
620
712
  try {
621
713
  const { llmConfigStore, PROVIDER_INFO } = await import('./llm/config-store.js');
622
714
  await llmConfigStore.initialize();
@@ -644,6 +736,52 @@ async function processInput(input, comm) {
644
736
  }
645
737
  return;
646
738
  }
739
+ // /login — GitHub / Google 账号登录骨架 (2026-08-08, 无真实 OAuth, 先做选择 + 记录)
740
+ if (cmd === '/login') {
741
+ try {
742
+ const { readFile, writeFile, mkdir } = await import('fs/promises');
743
+ const { join } = await import('path');
744
+ const home = process.env.HOME || '/tmp';
745
+ const accPath = join(home, '.bolloon', 'accounts.json');
746
+ let accs = [];
747
+ try {
748
+ const parsed = JSON.parse(await readFile(accPath, 'utf-8'));
749
+ accs = Array.isArray(parsed) ? parsed : [];
750
+ }
751
+ catch { /* 无 */ }
752
+ const gh = accs.filter((a) => a.provider === 'github');
753
+ const gg = accs.filter((a) => a.provider === 'google');
754
+ const items = [
755
+ { kind: 'command', label: 'GitHub', hint: gh.length ? `已登录 ${gh.length} 个账号` : '未登录', insert: 'GitHub' },
756
+ { kind: 'command', label: 'Google', hint: gg.length ? `已登录 ${gg.length} 个账号` : '未登录', insert: 'Google' },
757
+ ];
758
+ globalThis.__inkOpenPicker?.(items, '登录账号 (骨架) · 选择服务 · Esc 取消', async (it) => {
759
+ const provider = it.label.toLowerCase();
760
+ try {
761
+ const existing = accs.find((a) => a.provider === provider);
762
+ if (existing) {
763
+ appendLine(`${C_OK}✓ ${it.label}: 已登录${RESET} 账号=${existing.username || existing.email || '?'} (${existing.loggedAt || ''})`);
764
+ appendLine(` ${C_DIM}token: ${existing.token ? '已保存' : '无'} (未做真实 OAuth, 仅骨架)${RESET}`);
765
+ return;
766
+ }
767
+ // 骨架: 记录一个占位账号 (真实 OAuth 后续接入, 在此扩展)
768
+ const entry = { provider, username: `user-${provider}`, email: '', token: '', loggedAt: new Date().toISOString(), skeleton: true };
769
+ accs.push(entry);
770
+ await mkdir(join(home, '.bolloon'), { recursive: true });
771
+ await writeFile(accPath, JSON.stringify(accs, null, 2), 'utf-8');
772
+ appendLine(`${C_OK}✓ ${it.label} 登录骨架已记录 (未做真实 OAuth)${RESET}`);
773
+ appendLine(` ${C_DIM}后续接入: 这里会打开浏览器授权并交换 token${RESET}`);
774
+ }
775
+ catch (e) {
776
+ appendLine(`${C_ERROR}✗ /login 失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
777
+ }
778
+ });
779
+ }
780
+ catch (e) {
781
+ appendLine(`${C_ERROR}/login 失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
782
+ }
783
+ return;
784
+ }
647
785
  // /logout — 显示当前供应商 (减法: 登出 = 查看当前, 切换走 /model)
648
786
  if (cmd === '/logout') {
649
787
  try {
@@ -673,17 +811,22 @@ async function processInput(input, comm) {
673
811
  catch { /* 静默 */ }
674
812
  return;
675
813
  }
676
- // /tools — 可用工具列表
814
+ // /tools — 可用工具列表 (2026-08-08: 读 getToolList, 显示名 + 参数)
677
815
  if (cmd === '/tools') {
678
816
  try {
679
817
  const a = await getAgent();
680
- const defs = (a.getToolDefinitions?.() ?? []);
681
- const names = Array.isArray(defs) ? defs.map((d) => d.name || d.function?.name).filter(Boolean) : Object.keys(defs || {});
682
- appendLine(`${C_ACCENT}可用工具 (${names.length}):${RESET}`);
683
- for (const n of names.slice(0, 40))
684
- appendLine(` ${C_DIM}·${RESET} ${n}`);
685
- if (names.length > 40)
686
- appendLine(` ${C_DIM}... ${names.length} 个${RESET}`);
818
+ const list = a.getToolList?.() ?? [];
819
+ appendLine(`${C_ACCENT}可用工具 (${list.length}):${RESET}`);
820
+ if (list.length === 0) {
821
+ appendLine(` ${C_DIM}无 (agent 未初始化工具列表)${RESET}`);
822
+ }
823
+ for (const t of list.slice(0, 40)) {
824
+ const params = Array.isArray(t.parameters) && t.parameters.length > 0 ? `(${t.parameters.join(',')})` : '';
825
+ const desc = t.description ? ` ${C_DIM}${String(t.description).split('\n')[0].slice(0, 40)}${RESET}` : '';
826
+ appendLine(` ${C_DIM}·${RESET} ${t.name}${params}${desc}`);
827
+ }
828
+ if (list.length > 40)
829
+ appendLine(` ${C_DIM}... 共 ${list.length} 个${RESET}`);
687
830
  }
688
831
  catch { /* 静默 */ }
689
832
  return;
@@ -754,14 +897,14 @@ async function processInput(input, comm) {
754
897
  catch { /* 静默 */ }
755
898
  return;
756
899
  }
757
- // /goal — 进行中的目标/计划
900
+ // /goal — 进行中的目标/计划; /goal <文本> 设定新目标并触发循环 (2026-08-08)
758
901
  if (cmd === '/goal') {
759
902
  try {
760
- const { listActivePlans, planToContext } = await import('./agents/plan-store.js');
903
+ const { listActivePlans } = await import('./agents/plan-store.js');
761
904
  const plans = await listActivePlans();
762
905
  appendLine(`${C_ACCENT}目标 (${plans.length} 个进行中):${RESET}`);
763
906
  if (plans.length === 0) {
764
- appendLine(` ${C_DIM}无进行中计划 — 可用 /plan 创建${RESET}`);
907
+ appendLine(` ${C_DIM}无进行中计划 — 可用 /goal <目标> 设定 或 /plan 创建${RESET}`);
765
908
  }
766
909
  for (const p of plans.slice(0, 5)) {
767
910
  appendLine(` ${C_ACCENT}●${RESET} ${p.goal || p.planId} ${C_DIM}[${p.status || 'active'}]${RESET}`);
@@ -774,6 +917,102 @@ async function processInput(input, comm) {
774
917
  catch { /* 静默 */ }
775
918
  return;
776
919
  }
920
+ if (cmd.startsWith('/goal ')) {
921
+ const q = trimmed.slice('/goal '.length).trim();
922
+ try {
923
+ const { createPlan } = await import('./agents/plan-store.js');
924
+ const r = await createPlan({ goal: q, steps: [q], createdBy: 'user', originChannel: cliActiveChannelId || 'cli' });
925
+ if (!r.ok || !r.plan) {
926
+ appendLine(`${C_ERROR}/goal 设定失败: ${r.error || '未知'}${RESET}`);
927
+ return;
928
+ }
929
+ appendLine(`${C_OK}✓ 目标已设定: ${C_ACCENT}${q}${RESET} (${C_DIM}plan ${r.plan.planId}${RESET})`);
930
+ // 触发自我改进循环 (沙箱分支, 输出供用户审)
931
+ const { runSelfImproveLoop } = await import('./agents/pi-sdk-session-factory.js');
932
+ const loop = await runSelfImproveLoop(q).catch(() => ({ success: false, error: '未启动' }));
933
+ if (loop.success)
934
+ appendLine(` ${C_DIM}${loop.output}${RESET}`);
935
+ else
936
+ appendLine(` ${C_WARN}⚠ 循环未启动: ${loop.error}${RESET}`);
937
+ }
938
+ catch (e) {
939
+ appendLine(`${C_ERROR}/goal 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
940
+ }
941
+ return;
942
+ }
943
+ // /plan — 循环过程工具: 创建/查看计划 (2026-08-08)
944
+ // /plan <目标> :: <步骤1> | <步骤2> ... 创建
945
+ // /plan 查看进行中
946
+ if (cmd.startsWith('/plan ')) {
947
+ const q = trimmed.slice('/plan '.length).trim();
948
+ const [goalText, ...rest] = q.split('::');
949
+ const stepsFlat = rest.length > 0 ? rest[0] : '';
950
+ const steps = stepsFlat ? stepsFlat.split(/\s*[||]\s*/).map(s => s.trim()).filter(Boolean) : [goalText].filter(Boolean);
951
+ try {
952
+ const { createPlan } = await import('./agents/plan-store.js');
953
+ const r = await createPlan({ goal: goalText || q, steps: steps.length ? steps : [goalText], createdBy: 'user', originChannel: cliActiveChannelId || 'cli' });
954
+ if (!r.ok || !r.plan) {
955
+ appendLine(`${C_ERROR}/plan 创建失败: ${r.error || '未知'}${RESET}`);
956
+ return;
957
+ }
958
+ appendLine(`${C_OK}✓ 计划已创建: ${C_ACCENT}${r.plan.goal}${RESET} (${C_DIM}${r.plan.planId} · ${r.plan.steps.length} 步${RESET})`);
959
+ for (const s of r.plan.steps)
960
+ appendLine(` ${C_DIM}· ${s.description}${RESET}`);
961
+ }
962
+ catch (e) {
963
+ appendLine(`${C_ERROR}/plan 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
964
+ }
965
+ return;
966
+ }
967
+ // /todo — 循环过程工具: 查看/勾选步骤 (2026-08-08)
968
+ if (cmd === '/todo') {
969
+ try {
970
+ const { listActivePlans } = await import('./agents/plan-store.js');
971
+ const plans = await listActivePlans();
972
+ if (plans.length === 0) {
973
+ appendLine(`${C_DIM}无进行中计划 — 可用 /plan <目标> 创建${RESET}`);
974
+ return;
975
+ }
976
+ for (const p of plans.slice(0, 3)) {
977
+ appendLine(`${C_ACCENT}● ${p.goal}${RESET} ${C_DIM}[${p.status || 'active'}]${RESET}`);
978
+ const steps = Array.isArray(p.steps) ? p.steps : [];
979
+ for (let i = 0; i < steps.length; i++) {
980
+ const s = steps[i];
981
+ const done = s.status === 'done' || s.done;
982
+ appendLine(` ${done ? '✓' : '○'} ${i + 1}. ${s.description || ''}${done ? '' : ` ${C_DIM}/todo ${p.planId} ${i + 1} 勾选${RESET}`}`);
983
+ }
984
+ }
985
+ }
986
+ catch { /* 静默 */ }
987
+ return;
988
+ }
989
+ if (/^\/todo\s+\S+\s+\d+/.test(trimmed.toLowerCase())) {
990
+ const parts = trimmed.split(/\s+/);
991
+ const planId = parts[1];
992
+ const idx = parseInt(parts.slice(2).join(' ').trim(), 10) - 1 || 0;
993
+ try {
994
+ const { loadPlan, updatePlan } = await import('./agents/plan-store.js');
995
+ const plan = await loadPlan(planId);
996
+ if (!plan) {
997
+ appendLine(`${C_ERROR}/todo 失败: plan '${planId}' 不存在${RESET}`);
998
+ return;
999
+ }
1000
+ const step = plan.steps[idx];
1001
+ if (!step) {
1002
+ appendLine(`${C_ERROR}/todo 失败: 无第 ${idx + 1} 步${RESET}`);
1003
+ return;
1004
+ }
1005
+ await updatePlan(planId, { stepId: step.id, status: 'done' });
1006
+ const done = plan.steps.filter(s => s.status === 'done' || s.done).length + 1;
1007
+ const total = plan.steps.length;
1008
+ const allDone = done >= total;
1009
+ appendLine(`${C_OK}✓ 勾选 ${idx + 1}. ${step.description}${RESET} (${done}/${total})${allDone ? `\n ${C_ACCENT}🎯 循环达到完成标准, 可以 review 结束: /review ${planId}${RESET}` : ''}`);
1010
+ }
1011
+ catch (e) {
1012
+ appendLine(`${C_ERROR}/todo 失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
1013
+ }
1014
+ return;
1015
+ }
777
1016
  // /skill — 技能候选 (skill-writer 落盘)
778
1017
  if (cmd === '/skill') {
779
1018
  try {
@@ -899,14 +1138,15 @@ async function processInput(input, comm) {
899
1138
  catch { /* 静默 */ }
900
1139
  return;
901
1140
  }
902
- // /email — 邮件配置状态
1141
+ // /email — 邮件配置管理; /email <host:port:user:from> 设置 / /email clear 清除 (2026-08-08)
903
1142
  if (cmd === '/email') {
904
1143
  try {
905
- const { readFile } = await import('fs/promises');
1144
+ const { readFile, writeFile, mkdir } = await import('fs/promises');
906
1145
  const { join } = await import('path');
1146
+ const p = join(process.env.HOME || '/tmp', '.bolloon', 'smtp.json');
907
1147
  let cfg = null;
908
1148
  try {
909
- cfg = JSON.parse(await readFile(join(process.env.HOME || '/tmp', '.bolloon', 'smtp.json'), 'utf-8'));
1149
+ cfg = JSON.parse(await readFile(p, 'utf-8'));
910
1150
  }
911
1151
  catch { /* 无 */ }
912
1152
  appendLine(`${C_ACCENT}邮件 (SMTP):${RESET}`);
@@ -917,11 +1157,59 @@ async function processInput(input, comm) {
917
1157
  appendLine(` ${C_DIM}host:${RESET} ${cfg.host || 'smtp.qq.com'}`);
918
1158
  appendLine(` ${C_DIM}发件人:${RESET} ${cfg.from || cfg.user || '—'}`);
919
1159
  }
1160
+ appendLine(` ${C_DIM}用法: /email <host:port:user:from> 设置 · /email clear 清除 · /email pass <授权码> 设密码${RESET}`);
920
1161
  }
921
1162
  catch { /* 静默 */ }
922
1163
  return;
923
1164
  }
924
- // /loop — 当前循环状态 (消息数 + token 估算)
1165
+ if (cmd === '/email clear') {
1166
+ try {
1167
+ const { writeFile, mkdir } = await import('fs/promises');
1168
+ const { join } = await import('path');
1169
+ const p = join(process.env.HOME || '/tmp', '.bolloon', 'smtp.json');
1170
+ await mkdir(join(process.env.HOME || '/tmp', '.bolloon'), { recursive: true });
1171
+ await writeFile(p, '{}', 'utf-8');
1172
+ appendLine(`${C_OK}✓ smtp 配置已清除${RESET}`);
1173
+ }
1174
+ catch {
1175
+ appendLine(`${C_ERROR}/email clear 失败${RESET}`);
1176
+ }
1177
+ return;
1178
+ }
1179
+ if (cmd.startsWith('/email ')) {
1180
+ const q = trimmed.slice('/email '.length).trim();
1181
+ try {
1182
+ const { writeFile, mkdir } = await import('fs/promises');
1183
+ const { join } = await import('path');
1184
+ const p = join(process.env.HOME || '/tmp', '.bolloon', 'smtp.json');
1185
+ await mkdir(join(process.env.HOME || '/tmp', '.bolloon'), { recursive: true });
1186
+ let cfg = {};
1187
+ try {
1188
+ cfg = JSON.parse(await import('fs/promises').then(m => m.readFile(p, 'utf-8')));
1189
+ }
1190
+ catch { /* 无 */ }
1191
+ if (cmd.startsWith('/email pass')) {
1192
+ cfg.pass = q;
1193
+ await writeFile(p, JSON.stringify(cfg, null, 2), 'utf-8');
1194
+ appendLine(`${C_OK}✓ SMTP 授权码已保存${RESET}`);
1195
+ return;
1196
+ }
1197
+ const parts = q.split(':');
1198
+ if (parts.length >= 3) {
1199
+ cfg = { host: parts[0], port: parseInt(parts[1], 10) || 465, user: parts[2], from: parts[3] || parts[2], pass: cfg.pass };
1200
+ await writeFile(p, JSON.stringify(cfg, null, 2), 'utf-8');
1201
+ appendLine(`${C_OK}✓ SMTP 已设置: ${cfg.host}:${cfg.port} (发件人 ${cfg.from})${RESET}`);
1202
+ }
1203
+ else {
1204
+ appendLine(`${C_DIM}格式不对: /email <host:port:user:from> 或 /email clear 或 /email pass <授权码>${RESET}`);
1205
+ }
1206
+ }
1207
+ catch (e) {
1208
+ appendLine(`${C_ERROR}/email 设置失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
1209
+ }
1210
+ return;
1211
+ }
1212
+ // /loop — 当前循环状态; /loop <目标> <完成标准> 设目标+标准并启动循环 (2026-08-08)
925
1213
  if (cmd === '/loop') {
926
1214
  try {
927
1215
  const a = await getAgent();
@@ -937,10 +1225,42 @@ async function processInput(input, comm) {
937
1225
  appendLine(`${C_ACCENT}Loop 状态:${RESET}`);
938
1226
  appendLine(` ${C_DIM}消息:${RESET} ${h.length} 条 (窗口 15, ${Math.max(0, h.length - 15)} 条早期压缩)`);
939
1227
  appendLine(` ${C_DIM}token:${RESET} ${(tokens / 1000).toFixed(1)}k / 1M (${((tokens / 1_000_000) * 100).toFixed(2)}%)`);
1228
+ appendLine(` ${C_DIM}用法: /loop <目标> (| <完成标准>) — 设目标并循环, 达到标准自动结束${RESET}`);
940
1229
  }
941
1230
  catch { /* 静默 */ }
942
1231
  return;
943
1232
  }
1233
+ if (cmd.startsWith('/loop ')) {
1234
+ const q = trimmed.slice('/loop '.length).trim();
1235
+ const [goalText, criteriaText] = q.split(/\s*[||]\s*/).map(s => s.trim());
1236
+ try {
1237
+ const { createPlan } = await import('./agents/plan-store.js');
1238
+ const criterion = criteriaText ? `达到标准: ${criteriaText}` : '';
1239
+ const r = await createPlan({
1240
+ goal: goalText || q,
1241
+ steps: [goalText || q, criterion].filter(Boolean),
1242
+ createdBy: 'user',
1243
+ originChannel: cliActiveChannelId || 'cli',
1244
+ });
1245
+ if (!r.ok || !r.plan) {
1246
+ appendLine(`${C_ERROR}/loop 启动失败: ${r.error || '未知'}${RESET}`);
1247
+ return;
1248
+ }
1249
+ appendLine(`${C_OK}✓ 循环已启动: ${C_ACCENT}${goalText}${RESET}${criteriaText ? `\n ${C_DIM}完成标准: ${criteriaText}${RESET}` : ''} (${C_DIM}plan ${r.plan.planId}${RESET})`);
1250
+ appendLine(` ${C_DIM}当标准达成时用 /todo 勾选最后一步 / 或 review 后自动结束循环${RESET}`);
1251
+ // 启动自我改进循环
1252
+ const { runSelfImproveLoop } = await import('./agents/pi-sdk-session-factory.js');
1253
+ const loop = await runSelfImproveLoop(goalText).catch(() => ({ success: false, error: '未启动' }));
1254
+ if (loop.success)
1255
+ appendLine(` ${C_DIM}${loop.output}${RESET}`);
1256
+ else
1257
+ appendLine(` ${C_WARN}⚠ 循环未启动: ${loop.error}${RESET}`);
1258
+ }
1259
+ catch (e) {
1260
+ appendLine(`${C_ERROR}/loop 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
1261
+ }
1262
+ return;
1263
+ }
944
1264
  // /judgement — 判断力列表
945
1265
  if (cmd === '/judgement' || cmd === '/judgments') {
946
1266
  try {
@@ -992,7 +1312,7 @@ async function processInput(input, comm) {
992
1312
  catch { /* 静默 */ }
993
1313
  return;
994
1314
  }
995
- // /dream — 随机灵感 (从 Insights + Knowledge 资产随机取一条)
1315
+ // /dream — 随机灵感; /dream <主题> 把用户主题落盘到梦想文档并触发循环 (2026-08-08)
996
1316
  if (cmd === '/dream') {
997
1317
  try {
998
1318
  const { readContextAssets, readAssetBody } = await import('./bootstrap/context-os.js');
@@ -1014,25 +1334,63 @@ async function processInput(input, comm) {
1014
1334
  const pick = pool[Math.floor(Math.random() * pool.length)];
1015
1335
  appendLine(`${C_DIM}🌙 ${pick}${RESET}`);
1016
1336
  }
1337
+ appendLine(` ${C_DIM}用法: /dream <主题> — 把主题写入梦想文档并启动循环${RESET}`);
1017
1338
  }
1018
1339
  catch { /* 静默 */ }
1019
1340
  return;
1020
1341
  }
1342
+ if (cmd.startsWith('/dream ')) {
1343
+ const topic = trimmed.slice('/dream '.length).trim();
1344
+ try {
1345
+ const { writeFile, mkdir } = await import('fs/promises');
1346
+ const { join } = await import('path');
1347
+ const home = process.env.HOME || '/tmp';
1348
+ const dreamDir = join(home, '.bolloon', 'dreams');
1349
+ await mkdir(dreamDir, { recursive: true });
1350
+ // 梦想文档路径: 用户名 + 主题 → 文件名 (用户信息集成进路径)
1351
+ const userTag = (cliAgentName || 'user').toLowerCase().replace(/[^a-z0-9]+/g, '-');
1352
+ const safeTopic = topic.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-').slice(0, 40);
1353
+ const dreamPath = join(dreamDir, `${new Date().toISOString().slice(0, 10)}-${userTag}-${safeTopic}.md`);
1354
+ const doc = `# 🌙 Dream: ${topic}\n\ndate: ${new Date().toISOString()}\nuser: ${cliAgentName || 'user'}\nchannel: ${cliActiveChannelId || 'cli'}\n\n> 自动生成于 /dream, 触发循环去探索这个主题。\n`;
1355
+ await writeFile(dreamPath, doc, 'utf-8');
1356
+ appendLine(`${C_OK}✓ 梦想文档已写入: ${C_DIM}${dreamPath}${RESET}`);
1357
+ // 触发循环
1358
+ const { createPlan } = await import('./agents/plan-store.js');
1359
+ const r = await createPlan({ goal: `探索主题: ${topic}`, steps: [`研读 ${topic}`, '产出洞察'], createdBy: 'user', originChannel: cliActiveChannelId || 'cli' });
1360
+ if (r.ok)
1361
+ appendLine(` ${C_DIM}循环已关联 plan ${r.plan?.planId}${RESET}`);
1362
+ const { runSelfImproveLoop } = await import('./agents/pi-sdk-session-factory.js');
1363
+ const loop = await runSelfImproveLoop(`探索主题: ${topic}`).catch(() => ({ success: false, error: '未启动' }));
1364
+ if (loop.success)
1365
+ appendLine(` ${C_DIM}${loop.output}${RESET}`);
1366
+ else
1367
+ appendLine(` ${C_WARN}⚠ 循环未启动: ${loop.error}${RESET}`);
1368
+ }
1369
+ catch (e) {
1370
+ appendLine(`${C_ERROR}/dream 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
1371
+ }
1372
+ return;
1373
+ }
1021
1374
  if (trimmed.toLowerCase() === '/help' || trimmed === 'help') {
1022
1375
  appendLine(`${C_DIM}命令:${RESET}`);
1023
1376
  appendLine(` ${C_ACCENT}!<cmd>${RESET} 执行终端命令 ${C_DIM}如 !ls -la${RESET}`);
1024
1377
  appendLine(` ${C_ACCENT}/queue${RESET} 切换队列模式 ${C_DIM}输入排队, 当前结束后自动执行${RESET}`);
1025
1378
  appendLine(` ${C_ACCENT}/dequeue${RESET} 出队一条`);
1026
1379
  appendLine(` ${C_ACCENT}/channel [名字|id|序号]${RESET} 切换当前智能体 ${C_DIM}无参列出所有; 支持名字/ID/序号三种解析${RESET}`);
1027
- appendLine(` ${C_ACCENT}/model${RESET} / ${C_ACCENT}/login${RESET} 模型供应商选择器 ${C_DIM}↑↓ 选择 · Enter 确认 · Esc 取消${RESET}`);
1380
+ appendLine(` ${C_ACCENT}/model${RESET} 模型供应商选择器 ${C_DIM}↑↓ 选择 · Enter 确认 · Esc 取消${RESET}`);
1381
+ appendLine(` ${C_ACCENT}/login${RESET} 登录 GitHub/Google 账号 (骨架) ${C_DIM}暂无真实 OAuth${RESET}`);
1028
1382
  appendLine(` ${C_ACCENT}/logout${RESET} 查看当前供应商`);
1383
+ appendLine(` ${C_ACCENT}/new agent${RESET} 创建新智能体 channel ${C_DIM}/new agent <名字>${RESET}`);
1384
+ appendLine(` ${C_ACCENT}/new session${RESET} 开新会话 ${C_DIM}清空当前 channel 消息窗口${RESET}`);
1029
1385
  appendLine(` ${C_ACCENT}/now${RESET} 当前状态总览 ${C_DIM}智能体/运行时间/上下文 tokens/消息数${RESET}`);
1030
1386
  appendLine(` ${C_ACCENT}/session${RESET} 当前会话信息 ${C_DIM}channel/agent/消息窗口${RESET}`);
1031
- appendLine(` ${C_ACCENT}/loop${RESET} 循环状态 ${C_DIM}消息数 + token 估算${RESET}`);
1387
+ appendLine(` ${C_ACCENT}/loop${RESET} 循环状态/启动 ${C_DIM}/loop <目标> (| <完成标准>)${RESET}`);
1032
1388
  appendLine(` ${C_ACCENT}/memory${RESET} 记忆摘要 ${C_DIM}memory-compressor 落盘摘要${RESET}`);
1033
1389
  appendLine(` ${C_ACCENT}/resume${RESET} 恢复上下文 ${C_DIM}最近记忆 + 进行中计划${RESET}`);
1034
- appendLine(` ${C_ACCENT}/goal${RESET} 进行中目标 ${C_DIM}plan-store active plans${RESET}`);
1035
- appendLine(` ${C_ACCENT}/tools${RESET} 可用工具列表`);
1390
+ appendLine(` ${C_ACCENT}/goal${RESET} 查看/设定目标 ${C_DIM}/goal 查看 · /goal <目标> 设定+循环${RESET}`);
1391
+ appendLine(` ${C_ACCENT}/plan${RESET} 创建计划 ${C_DIM}/plan <目标> :: <步骤1>|<步骤2>${RESET}`);
1392
+ appendLine(` ${C_ACCENT}/todo${RESET} 查看/勾选循环步骤 ${C_DIM}/todo <planId> <序号>${RESET}`);
1393
+ appendLine(` ${C_ACCENT}/tools${RESET} 可用工具列表 (名/参数/简介)`);
1036
1394
  appendLine(` ${C_ACCENT}/skill${RESET} 技能候选 ${C_DIM}skill-writer 沉淀候选${RESET}`);
1037
1395
  appendLine(` ${C_ACCENT}/mcp${RESET} MCP 服务器列表`);
1038
1396
  appendLine(` ${C_ACCENT}/agent${RESET} 当前智能体身份`);
@@ -1040,11 +1398,11 @@ async function processInput(input, comm) {
1040
1398
  appendLine(` ${C_ACCENT}/ipfs${RESET} Kubo 状态 ${C_DIM}节点/peers/pins${RESET}`);
1041
1399
  appendLine(` ${C_ACCENT}/ipns${RESET} IPNS keys + resolve`);
1042
1400
  appendLine(` ${C_ACCENT}/wallet${RESET} 钱包状态`);
1043
- appendLine(` ${C_ACCENT}/email${RESET} 邮件配置`);
1401
+ appendLine(` ${C_ACCENT}/email${RESET} 邮件配置管理 ${C_DIM}/email 查看 · <host:port:user:from> 设置 · clear 清除${RESET}`);
1044
1402
  appendLine(` ${C_ACCENT}/judgement${RESET} 判断力列表`);
1045
1403
  appendLine(` ${C_ACCENT}/insight${RESET} Context OS 洞察 (08-Insights)`);
1046
1404
  appendLine(` ${C_ACCENT}/wiki${RESET} wiki 状态`);
1047
- appendLine(` ${C_ACCENT}/dream${RESET} 随机灵感`);
1405
+ appendLine(` ${C_ACCENT}/dream${RESET} 随机灵感 ${C_DIM}/dream <主题> 写入梦想文档并触发循环${RESET}`);
1048
1406
  appendLine(` ${C_ACCENT}@名字${RESET} @ 命中智能体 ${C_DIM}弹出窗选择后发送给智能体${RESET}`);
1049
1407
  appendLine(` ${C_ACCENT}/名字${RESET} / 命中命令/技能/插件 ${C_DIM}输入 / 自动弹出${RESET}`);
1050
1408
  appendLine(` ${C_ACCENT}#路径${RESET} # 命中文件 ${C_DIM}输入 # 自动弹出文件列表${RESET}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.41",
3
+ "version": "0.3.43",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",