@bolloon/bolloon-agent 0.3.52 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +135 -135
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon.cjs +0 -0
  4. package/bin/bolloon.js +157 -0
  5. package/dist/agents/constraint-layer.js +19 -19
  6. package/dist/agents/judgment-protocol.js +14 -14
  7. package/dist/agents/loop-review.js +21 -0
  8. package/dist/agents/pi-sdk-tools.js +86 -2
  9. package/dist/agents/pi-sdk-types.js +12 -12
  10. package/dist/agents/pi-sdk.js +90 -90
  11. package/dist/agents/shell-guard.js +22 -1
  12. package/dist/agents/skill-organizer.js +19 -19
  13. package/dist/agents/verify-recipe.js +84 -0
  14. package/dist/agents/workflow-pivot-loop.js +101 -13
  15. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  16. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  17. package/dist/bollharness-integration/context-router.js +292 -292
  18. package/dist/bollharness-integration/gate-state-machine.js +13 -13
  19. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  20. package/dist/bollharness-integration/skill-adapter.js +21 -21
  21. package/dist/bootstrap/chat-archiver.js +14 -14
  22. package/dist/bootstrap/context-os.js +50 -40
  23. package/dist/bootstrap/memory-compressor.js +13 -13
  24. package/dist/cli-entry.js +36 -36
  25. package/dist/context-compaction/auto-compact.js +7 -7
  26. package/dist/cron/cron-parser.js +142 -0
  27. package/dist/cron/jobs-store.js +96 -0
  28. package/dist/cron/scheduler.js +72 -0
  29. package/dist/cron/suggestion-catalog.js +41 -0
  30. package/dist/cron/suggestions-command.js +96 -0
  31. package/dist/cron/suggestions.js +110 -0
  32. package/dist/electron/first-run.js +54 -54
  33. package/dist/electron-build/electron/first-run.js +54 -54
  34. package/dist/electron-preload.js.map +1 -1
  35. package/dist/electron.js.map +1 -1
  36. package/dist/external-engines/delegate-handle.js +70 -0
  37. package/dist/external-engines/delegate.js +24 -3
  38. package/dist/index.js +219 -112
  39. package/dist/llm/error-lessons.js +69 -0
  40. package/dist/llm/llm-judgment-client.js +102 -102
  41. package/dist/llm/pi-ai.js +86 -48
  42. package/dist/llm/system-prompt/layers/channel/human-async.md +41 -41
  43. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  44. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  45. package/dist/llm/system-prompt/layers/channel/p2p-peer-sync.md +51 -51
  46. package/dist/llm/system-prompt/layers/channel/p2p-proactive.md +43 -43
  47. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  48. package/dist/llm/system-prompt/layers/channel/session-handoff.md +61 -61
  49. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  50. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  51. package/dist/llm/system-prompt/layers/core/external-engagement.md +73 -73
  52. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  53. package/dist/llm/system-prompt/layers/core/identity.md +48 -48
  54. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  55. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  56. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  57. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  58. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  59. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  60. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  61. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  62. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  63. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  64. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  65. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  66. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  67. package/dist/llm/system-prompt/layers/tool/goal_handoff.md +77 -77
  68. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  69. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  70. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  71. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  72. package/dist/llm/system-prompt/layers/tool/p2p_request.md +61 -61
  73. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  74. package/dist/llm/system-prompt/registry.js +9 -1
  75. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  76. package/dist/llm/tool-manifest/bash.js +3 -3
  77. package/dist/llm/tool-manifest/create_file.js +4 -4
  78. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  79. package/dist/llm/tool-manifest/image_search.js +2 -2
  80. package/dist/llm/tool-manifest/index.js +0 -59
  81. package/dist/llm/tool-manifest/mcp.js +2 -2
  82. package/dist/llm/tool-manifest/message_compose.js +3 -3
  83. package/dist/llm/tool-manifest/places.js +2 -2
  84. package/dist/llm/tool-manifest/present_files.js +1 -1
  85. package/dist/llm/tool-manifest/recipe.js +2 -2
  86. package/dist/llm/tool-manifest/str_replace.js +5 -5
  87. package/dist/llm/tool-manifest/view.js +3 -3
  88. package/dist/llm/tool-manifest/weather.js +4 -4
  89. package/dist/llm/tool-manifest/web.js +4 -4
  90. package/dist/locales/en.js +28 -0
  91. package/dist/locales/zh.js +26 -0
  92. package/dist/pi-ecosystem-colony/index.js +365 -0
  93. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  94. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  95. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  96. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  97. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  98. package/dist/security/context-router-tool.js +15 -15
  99. package/dist/social/ant-colony/AdaptiveHeartbeat.js +101 -0
  100. package/dist/social/ant-colony/PheromoneEngine.js +227 -0
  101. package/dist/social/ant-colony/index.js +6 -0
  102. package/dist/social/ant-colony/types.js +24 -0
  103. package/dist/storage/trajectory.js +101 -0
  104. package/dist/test/ai-judgment-test.js +80 -0
  105. package/dist/test/bollharness-integration.test.js +318 -0
  106. package/dist/test/channel-agent-multi-dialogue.js +205 -0
  107. package/dist/test/channel-heartbeat-agent-test.js +201 -0
  108. package/dist/test/constraint-layer.test.js +164 -0
  109. package/dist/test/diap-identity-test.js +172 -0
  110. package/dist/test/diap-quick-test.js +62 -0
  111. package/dist/test/global-shared-context.test.js +315 -0
  112. package/dist/test/harness-judgment-injection.test.js +246 -0
  113. package/dist/test/harness-workflow-integrator-test.js +228 -0
  114. package/dist/test/human-value-store.test.js +243 -0
  115. package/dist/test/hybrid-integration-test.js +118 -0
  116. package/dist/test/hybrid-messenger-verify.js +55 -0
  117. package/dist/test/iroh-bistream-debug.js +38 -0
  118. package/dist/test/iroh-communication.test.js +66 -0
  119. package/dist/test/iroh-debug-test.js +57 -0
  120. package/dist/test/iroh-diap-test.js +71 -0
  121. package/dist/test/iroh-direct-connect.js +55 -0
  122. package/dist/test/iroh-e2e-fixed.js +89 -0
  123. package/dist/test/iroh-e2e-same-process.js +63 -0
  124. package/dist/test/iroh-e2e.js +66 -0
  125. package/dist/test/iroh-final-e2e.js +72 -0
  126. package/dist/test/iroh-relay-test.js +37 -0
  127. package/dist/test/iroh-simple-test.js +41 -0
  128. package/dist/test/iroh-transport-verify.js +54 -0
  129. package/dist/test/iroh-transport.test.js +37 -0
  130. package/dist/test/iroh-two-nodes.js +70 -0
  131. package/dist/test/iroh-verify.js +44 -0
  132. package/dist/test/judgment-decision.test.js +219 -0
  133. package/dist/test/llm-judgment-integration.test.js +220 -0
  134. package/dist/test/p2p-agent-complex-dialogue.js +385 -0
  135. package/dist/test/p2p-agent-dialogue.js +341 -0
  136. package/dist/test/p2p-agent-full-bidirectional.js +510 -0
  137. package/dist/test/p2p-agent-harness-flow.js +437 -0
  138. package/dist/test/p2p-agent-harness-single.js +143 -0
  139. package/dist/test/p2p-ai-dialogue-test.js +318 -0
  140. package/dist/test/p2p-cid-connect-test.js +195 -0
  141. package/dist/test/p2p-connect-receiver.js +69 -0
  142. package/dist/test/p2p-doc-transfer.js +110 -0
  143. package/dist/test/p2p-identity-page-test.js +77 -0
  144. package/dist/test/p2p-iroh-test.js +171 -0
  145. package/dist/test/p2p-minimal-test.js +241 -0
  146. package/dist/test/p2p-node-1.js +148 -0
  147. package/dist/test/p2p-node-2.js +148 -0
  148. package/dist/test/p2p-server.js +281 -0
  149. package/dist/test/p2p-two-nodes-test.js +438 -0
  150. package/dist/test/pi-sdk.test.js +44 -0
  151. package/dist/test/set-persona.js +40 -0
  152. package/dist/test/simple.test.js +9 -0
  153. package/dist/test/storage-integration.test.js +150 -0
  154. package/dist/test/subagent-manager.test.js +276 -0
  155. package/dist/test/test-gate-flow.test.js +81 -0
  156. package/dist/test/workflow-engine.test.js +87 -0
  157. package/dist/test/workflow-pivot-loop.test.js +246 -0
  158. package/dist/web/api-config.html +779 -779
  159. package/dist/web/client-hearth.js +10 -10
  160. package/dist/web/components/wallet-viem.mjs +118 -118
  161. package/dist/web/delivery-ledger.js +103 -0
  162. package/dist/web/i18n.js +104 -0
  163. package/dist/web/index.html +472 -472
  164. package/dist/web/loop-noise.js +86 -0
  165. package/dist/web/manifest.json +20 -20
  166. package/dist/web/routes-tasks.js +242 -24
  167. package/dist/web/server-sse.js +10 -5
  168. package/dist/web/server-storage.js +111 -0
  169. package/dist/web/server.js +55 -26
  170. package/dist/web/style.css +4946 -4946
  171. package/dist/web/task-breaker.js +18 -0
  172. package/dist/web/task-cancel.js +35 -0
  173. package/dist/web/task-deps.js +36 -0
  174. package/dist/web/task-review.js +27 -0
  175. package/dist/web/task-verify.js +60 -0
  176. package/package.json +196 -196
  177. package/scripts/build-cli.js +215 -215
  178. package/scripts/build-web.ts +130 -130
  179. package/scripts/postinstall.js +152 -152
  180. package/bin/bolloon-daemon.sh +0 -207
  181. package/dist/constraint-runtime/reference_data/archive_surface_snapshot.json +0 -63
  182. package/dist/constraint-runtime/reference_data/commands_snapshot.json +0 -1037
  183. package/dist/constraint-runtime/reference_data/subsystems/assistant.json +0 -8
  184. package/dist/constraint-runtime/reference_data/subsystems/bootstrap.json +0 -8
  185. package/dist/constraint-runtime/reference_data/subsystems/bridge.json +0 -32
  186. package/dist/constraint-runtime/reference_data/subsystems/buddy.json +0 -13
  187. package/dist/constraint-runtime/reference_data/subsystems/cli.json +0 -26
  188. package/dist/constraint-runtime/reference_data/subsystems/components.json +0 -32
  189. package/dist/constraint-runtime/reference_data/subsystems/constants.json +0 -28
  190. package/dist/constraint-runtime/reference_data/subsystems/coordinator.json +0 -8
  191. package/dist/constraint-runtime/reference_data/subsystems/entrypoints.json +0 -15
  192. package/dist/constraint-runtime/reference_data/subsystems/hooks.json +0 -32
  193. package/dist/constraint-runtime/reference_data/subsystems/keybindings.json +0 -21
  194. package/dist/constraint-runtime/reference_data/subsystems/memdir.json +0 -15
  195. package/dist/constraint-runtime/reference_data/subsystems/migrations.json +0 -18
  196. package/dist/constraint-runtime/reference_data/subsystems/moreright.json +0 -8
  197. package/dist/constraint-runtime/reference_data/subsystems/native_ts.json +0 -11
  198. package/dist/constraint-runtime/reference_data/subsystems/outputStyles.json +0 -8
  199. package/dist/constraint-runtime/reference_data/subsystems/plugins.json +0 -9
  200. package/dist/constraint-runtime/reference_data/subsystems/remote.json +0 -11
  201. package/dist/constraint-runtime/reference_data/subsystems/schemas.json +0 -8
  202. package/dist/constraint-runtime/reference_data/subsystems/screens.json +0 -10
  203. package/dist/constraint-runtime/reference_data/subsystems/server.json +0 -10
  204. package/dist/constraint-runtime/reference_data/subsystems/services.json +0 -32
  205. package/dist/constraint-runtime/reference_data/subsystems/skills.json +0 -27
  206. package/dist/constraint-runtime/reference_data/subsystems/state.json +0 -13
  207. package/dist/constraint-runtime/reference_data/subsystems/types.json +0 -18
  208. package/dist/constraint-runtime/reference_data/subsystems/upstreamproxy.json +0 -9
  209. package/dist/constraint-runtime/reference_data/subsystems/utils.json +0 -32
  210. package/dist/constraint-runtime/reference_data/subsystems/vim.json +0 -12
  211. package/dist/constraint-runtime/reference_data/subsystems/voice.json +0 -8
  212. package/dist/constraint-runtime/reference_data/tools_snapshot.json +0 -922
  213. package/dist/electron/config.js.map +0 -1
  214. package/dist/electron/dialogs.js.map +0 -1
  215. package/dist/electron/first-run.js.map +0 -1
  216. package/dist/electron/ipc.js.map +0 -1
  217. package/dist/electron/logger.js.map +0 -1
  218. package/dist/electron/main.js.map +0 -1
  219. package/dist/electron/menu.js.map +0 -1
  220. package/dist/electron/paths.js.map +0 -1
  221. package/dist/electron/server.js.map +0 -1
  222. package/dist/electron/tray.js.map +0 -1
  223. package/dist/electron/window.js.map +0 -1
  224. package/dist/utils/auto-update.js.map +0 -1
  225. package/dist/web/client.js.map +0 -7
@@ -0,0 +1,142 @@
1
+ /**
2
+ * cron-parser.ts — cron 表达式 / 间隔解析 + 下次运行时间计算
3
+ *
4
+ * 借鉴 Hermes agent cron/scheduler.py 的调度心智:
5
+ * - 支持标准 5 段 cron ("0 8 * * *") 与人类可读间隔 ("every 30m", "1h", "30s")
6
+ * - 纯函数, 无 IO, 可单测: parseSchedule(schedule, from) → 下次触发 Date
7
+ * - schedule 为空 / 解析失败 → 返回 null (调用方决定不调度或报错)
8
+ */
9
+ /** "every 30m" / "every 1h" / "30s" / "1h" / "90s" → 毫秒; 其它返回 null */
10
+ function _parseIntervalMs(raw) {
11
+ const m = raw.toLowerCase().trim().match(/^every\s+(\d+)\s*(s|m|h|d)?$|^(\d+)\s*(s|m|h|d)$/);
12
+ if (!m)
13
+ return null;
14
+ const num = Number(m[1] ?? m[3]);
15
+ const unit = (m[2] ?? m[4]) || 'm';
16
+ const mult = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
17
+ return num * mult[unit];
18
+ }
19
+ /** 把 5 段 cron 拆成字段集合. 支持 *, 数字, 逗号, 范围, 步进 (仅分钟/小时/月/周日). */
20
+ function _parseCronFields(expr) {
21
+ const parts = expr.trim().split(/\s+/);
22
+ if (parts.length !== 5)
23
+ return null;
24
+ const parse = (field, lo, hi) => {
25
+ const set = new Set();
26
+ for (const seg of field.split(',')) {
27
+ if (seg === '*') {
28
+ for (let v = lo; v <= hi; v++)
29
+ set.add(v);
30
+ continue;
31
+ }
32
+ let step = 1;
33
+ let range = seg;
34
+ if (seg.includes('/')) {
35
+ const [r, s] = seg.split('/');
36
+ range = r;
37
+ step = Number(s) || 1;
38
+ }
39
+ let start = lo, end = hi;
40
+ if (range === '*') {
41
+ start = lo;
42
+ end = hi;
43
+ }
44
+ else if (range.includes('-')) {
45
+ const [a, b] = range.split('-');
46
+ start = Number(a);
47
+ end = Number(b);
48
+ }
49
+ else {
50
+ start = end = Number(range);
51
+ }
52
+ if (Number.isNaN(start) || Number.isNaN(end) || Number.isNaN(step))
53
+ return null;
54
+ for (let v = start; v <= end; v += step)
55
+ set.add(v);
56
+ }
57
+ return set;
58
+ };
59
+ const min = parse(parts[0], 0, 59);
60
+ const hour = parse(parts[1], 0, 23);
61
+ const dom = parse(parts[2], 1, 31);
62
+ const mon = parse(parts[3], 1, 12);
63
+ const dow = parse(parts[4], 0, 6);
64
+ if (!min || !hour || !dom || !mon || !dow)
65
+ return null;
66
+ return { min, hour, dom, mon, dow };
67
+ }
68
+ function _matchesCron(d, f) {
69
+ const domMatch = f.dom.has(d.getDate());
70
+ const dowMatch = f.dow.has(d.getDay());
71
+ // 经典 cron 语义: dom 与 dow 都受限时用 OR, 否则各自 AND
72
+ const domRestricted = !f.dom.has(31) && f.dom.size < 31;
73
+ const dowRestricted = f.dow.size < 7;
74
+ const dayOk = domRestricted && dowRestricted ? domMatch || dowMatch : domMatch && dowMatch;
75
+ return (dayOk &&
76
+ f.mon.has(d.getMonth() + 1) &&
77
+ f.hour.has(d.getHours()) &&
78
+ f.min.has(d.getMinutes()));
79
+ }
80
+ /**
81
+ * 解析 schedule 并计算下次触发时间.
82
+ * @param schedule "0 8 * * *" / "every 30m" / "1h" / "30s"
83
+ * @param from 基准时间 (默认 now)
84
+ * @returns ParsedSchedule 或 null (无法解析 / 未来 1 年内无匹配)
85
+ */
86
+ export function parseSchedule(schedule, from = new Date()) {
87
+ const raw = (schedule || '').trim();
88
+ if (!raw)
89
+ return null;
90
+ // 间隔形式 (最快路径)
91
+ const intervalMs = _parseIntervalMs(raw);
92
+ if (intervalMs != null && intervalMs > 0) {
93
+ return { kind: 'interval', intervalMs, next: new Date(from.getTime() + intervalMs) };
94
+ }
95
+ // 5 段 cron: 从 from 起逐分钟扫描, 找下一个匹配 (上限 1 年避免死循环)
96
+ const fields = _parseCronFields(raw);
97
+ if (!fields)
98
+ return null;
99
+ const cursor = new Date(from);
100
+ cursor.setSeconds(0, 0);
101
+ cursor.setMinutes(cursor.getMinutes() + 1); // 严格 "下一次", 不含当前分
102
+ const capMs = from.getTime() + 366 * 86_400_000;
103
+ while (cursor.getTime() < capMs) {
104
+ if (_matchesCron(cursor, fields)) {
105
+ return { kind: 'cron', expr: raw, next: cursor, fields };
106
+ }
107
+ cursor.setMinutes(cursor.getMinutes() + 1);
108
+ }
109
+ return null;
110
+ }
111
+ /**
112
+ * 刷新一次调度: 给定 schedule 与上次触发, 返回本次应跑的触发点.
113
+ * 间隔型 → 以 lastRunAt 为基准叠加; 无 lastRunAt (首次) → 立即 due (返回 now).
114
+ * cron 型 → 从 max(lastRunAt, now) 起找匹配; 上次触发已过期 → 立即 due.
115
+ */
116
+ export function nextAfter(schedule, lastRunAt, now = new Date()) {
117
+ const raw = (schedule || '').trim();
118
+ if (!raw)
119
+ return null;
120
+ const intervalMs = _parseIntervalMs(raw);
121
+ if (intervalMs != null && intervalMs > 0) {
122
+ if (!lastRunAt)
123
+ return now; // 首次: 立即触发
124
+ return new Date(lastRunAt.getTime() + intervalMs);
125
+ }
126
+ // cron 型: 依托上次触发后的下一次, 与"当前墙钟"比较
127
+ const fields = _parseCronFields(raw);
128
+ if (!fields)
129
+ return null;
130
+ const base = lastRunAt && lastRunAt > now ? new Date(lastRunAt) : new Date(now);
131
+ base.setSeconds(0, 0);
132
+ let cursor = new Date(base);
133
+ if (lastRunAt)
134
+ cursor.setMinutes(cursor.getMinutes() + 1); // 严格晚于上次触发
135
+ const capMs = base.getTime() + 366 * 86_400_000;
136
+ while (cursor.getTime() < capMs) {
137
+ if (_matchesCron(cursor, fields))
138
+ return cursor;
139
+ cursor.setMinutes(cursor.getMinutes() + 1);
140
+ }
141
+ return null;
142
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * jobs-store.ts — 定时任务 (schedule jobs) 持久化
3
+ *
4
+ * 借鉴 Hermes agent cron/scheduler.py 心智:
5
+ * - 每条 job 有: id / name / schedule (cron 或 every N) / prompt (执行体) / enabled
6
+ * - 记录 lastRunAt / runCount, 调度器 tick 时找出 due 的 job 执行
7
+ * - 落盘 ~/.bolloon/cron-jobs.json, 进程内互斥链序列化 + 临时文件原子写
8
+ * - 不建数据库, 不引入外部调度系统 —— 单文件 + 内存互斥
9
+ */
10
+ import * as fs from 'fs/promises';
11
+ import * as os from 'os';
12
+ import * as path from 'path';
13
+ import * as crypto from 'crypto';
14
+ function getJobsPath(home = os.homedir()) {
15
+ return path.join(home, '.bolloon', 'cron-jobs.json');
16
+ }
17
+ let jobsLock = Promise.resolve();
18
+ function withLock(fn) {
19
+ const run = jobsLock.then(fn);
20
+ jobsLock = run.then(() => undefined, () => undefined);
21
+ return run;
22
+ }
23
+ async function readAll(home) {
24
+ try {
25
+ const raw = await fs.readFile(getJobsPath(home), 'utf-8');
26
+ const p = JSON.parse(raw);
27
+ if (p?.version === 1 && Array.isArray(p.jobs))
28
+ return p;
29
+ return { version: 1, jobs: [] };
30
+ }
31
+ catch {
32
+ return { version: 1, jobs: [] };
33
+ }
34
+ }
35
+ async function writeAll(file, home) {
36
+ const p = getJobsPath(home);
37
+ await fs.mkdir(path.dirname(p), { recursive: true });
38
+ const tmp = p + '.tmp';
39
+ await fs.writeFile(tmp, JSON.stringify(file, null, 2), 'utf-8');
40
+ await fs.rename(tmp, p);
41
+ }
42
+ export async function addJob(input, home = os.homedir()) {
43
+ return withLock(async () => {
44
+ const file = await readAll(home);
45
+ const job = {
46
+ id: crypto.randomUUID(),
47
+ name: input.name,
48
+ schedule: input.schedule,
49
+ prompt: input.prompt,
50
+ enabled: true,
51
+ createdAt: new Date().toISOString(),
52
+ runCount: 0,
53
+ };
54
+ file.jobs.push(job);
55
+ await writeAll(file, home);
56
+ return job;
57
+ });
58
+ }
59
+ export async function listJobs(home = os.homedir()) {
60
+ return withLock(async () => (await readAll(home)).jobs);
61
+ }
62
+ export async function removeJob(id, home = os.homedir()) {
63
+ return withLock(async () => {
64
+ const file = await readAll(home);
65
+ const before = file.jobs.length;
66
+ file.jobs = file.jobs.filter((j) => j.id !== id);
67
+ if (file.jobs.length === before)
68
+ return false;
69
+ await writeAll(file, home);
70
+ return true;
71
+ });
72
+ }
73
+ export async function setEnabled(id, enabled, home = os.homedir()) {
74
+ return withLock(async () => {
75
+ const file = await readAll(home);
76
+ const job = file.jobs.find((j) => j.id === id);
77
+ if (!job)
78
+ return null;
79
+ job.enabled = enabled;
80
+ await writeAll(file, home);
81
+ return job;
82
+ });
83
+ }
84
+ /** 标记已运行: 更新 lastRunAt + runCount (runId 用于并行保护, 由 scheduler 提供) */
85
+ export async function markRun(id, at, home = os.homedir()) {
86
+ return withLock(async () => {
87
+ const file = await readAll(home);
88
+ const job = file.jobs.find((j) => j.id === id);
89
+ if (!job)
90
+ return null;
91
+ job.lastRunAt = at;
92
+ job.runCount += 1;
93
+ await writeAll(file, home);
94
+ return job;
95
+ });
96
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * scheduler.ts — 定时任务调度器
3
+ *
4
+ * 借鉴 Hermes agent cron/scheduler.py 心智:
5
+ * - 一个 tick() 扫描全部 enabled jobs, 用 parseSchedule 判断是否 due (下次时间 <= now)
6
+ * - due → 执行 exec(job), 完成后 markRun 更新 lastRunAt
7
+ * - 并发保护: 正在运行的 jobId 集合, 避免同一 job 并行重复触发
8
+ * - exec 可注入: bolloon CLI 里把 job.prompt 投给 agent 调度闭环, 测试里可注入 fake
9
+ * - 不建数据库, 不引入外部调度 —— 由调用方 (CLI 主循环) 定期调 tick()
10
+ */
11
+ import * as os from 'os';
12
+ import { listJobs, markRun } from './jobs-store.js';
13
+ import { parseSchedule, nextAfter } from './cron-parser.js';
14
+ export class Scheduler {
15
+ exec;
16
+ now;
17
+ home;
18
+ /** 正在运行的 job id, 防止并行重复触发 */
19
+ running = new Set();
20
+ /** 已触发但执行失败, 待下次重试 (避免死循环 + 可观测) */
21
+ failed = new Map();
22
+ constructor(opts) {
23
+ this.exec = opts.exec;
24
+ this.now = opts.now ?? (() => new Date());
25
+ this.home = opts.home ?? os.homedir();
26
+ }
27
+ /** 是否正在运行某 job (供外部查询并发状态) */
28
+ isRunning(jobId) {
29
+ return this.running.has(jobId);
30
+ }
31
+ /**
32
+ * 一次调度心跳. 找出所有 due 的 enabled job 并执行 (串行), 成功者 markRun.
33
+ * @returns 本次执行数量
34
+ */
35
+ async tick() {
36
+ const now = this.now();
37
+ const jobs = await listJobs(this.home);
38
+ let ran = 0;
39
+ for (const job of jobs) {
40
+ if (!job.enabled)
41
+ continue;
42
+ if (this.running.has(job.id))
43
+ continue; // 正在跑, 跳过防重入
44
+ const parsed = parseSchedule(job.schedule, now);
45
+ if (!parsed)
46
+ continue; // 解析不了的调度, 跳过 (可另做告警)
47
+ const lastRunAt = job.lastRunAt ? new Date(job.lastRunAt) : undefined;
48
+ const due = nextAfter(job.schedule, lastRunAt, now);
49
+ if (due && due.getTime() > now.getTime())
50
+ continue; // 还不到时间 (含首次: 立即触发)
51
+ this.running.add(job.id);
52
+ try {
53
+ await this.exec(job);
54
+ await markRun(job.id, now.toISOString(), this.home);
55
+ this.failed.delete(job.id);
56
+ ran += 1;
57
+ }
58
+ catch (e) {
59
+ this.failed.set(job.id, (this.failed.get(job.id) ?? 0) + 1);
60
+ console.error(`[scheduler] job 执行失败 (${job.name}):`, e?.message ?? e);
61
+ }
62
+ finally {
63
+ this.running.delete(job.id);
64
+ }
65
+ }
66
+ return ran;
67
+ }
68
+ /** 最近失败统计 (供诊断) */
69
+ failureCounts() {
70
+ return this.failed;
71
+ }
72
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * suggestion-catalog.ts — 内置"下一动作"建议目录
3
+ *
4
+ * 借鉴 Hermes agent suggestion_catalog.py 心智:
5
+ * - 不是让用户每次从零想"该做什么", 而是提供一组成熟、可一配置就用的自动化/策略建议
6
+ * - 每个条目是 { dedupKey, title, summary, schedule?, prompt? }
7
+ * - CLI `/suggestions catalog` 可列出, 用户任选自动装机 (装成 suggestion 或 cron job)
8
+ */
9
+ export const SUGGESTION_CATALOG = [
10
+ {
11
+ dedupKey: 'daily-review',
12
+ title: '每日工作复盘',
13
+ summary: '每天工作结束后 5 分钟, 让 agent 总结当天做的事、遗留事项、并写进 wiki 日志。',
14
+ schedule: '0 18 * * *',
15
+ prompt: '对今天的会话做一次工作复盘: 列出完成的任务、遗留的阻塞项、并追加一行到 wiki 日志。',
16
+ },
17
+ {
18
+ dedupKey: 'weekly-planner',
19
+ title: '每周计划',
20
+ summary: '每周一早上生成本周重点任务清单, 纳入当前计划。',
21
+ schedule: '0 9 * * 1',
22
+ prompt: '生成本周计划: 结合最近 wiki 状态与未完成任务, 列出 3-5 条本周重点。',
23
+ },
24
+ {
25
+ dedupKey: 'wiki-sync',
26
+ title: 'Wiki 一致性检查',
27
+ summary: '定期跑 wiki lint / stale 检查, 发现过期页面就建议修正。',
28
+ schedule: '0 12 * * 5',
29
+ prompt: '运行一次 wiki 一致性检查 (stale_report/wiki_lint), 把发现的过期页面整理成待办建议。',
30
+ },
31
+ {
32
+ dedupKey: 'version-check',
33
+ title: '版本更新检查',
34
+ summary: '定期检查 bolloon 是否有新版本并提示升级。',
35
+ schedule: 'every 1d',
36
+ prompt: '检查 bolloon 是否有新版本, 若有则提示用户升级。',
37
+ },
38
+ ];
39
+ export function findCatalogEntry(dedupKey) {
40
+ return SUGGESTION_CATALOG.find((c) => c.dedupKey === dedupKey);
41
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * suggestions-command.ts — `/suggestions` CLI 子命令处理 (纯函数返回文本)
3
+ *
4
+ * 借鉴 Hermes agent suggestions_cmd.py 心智: 一个入口, 多个子动作, 全部返回可回显的文本.
5
+ * 用法:
6
+ * /suggestions → 列出当前待处理建议
7
+ * /suggestions accept <n> → 接受第 n 条 (转成任务 / 消费)
8
+ * /suggestions dismiss <n>
9
+ * /suggestions clear → 全部清空
10
+ * /suggestions catalog → 列出内置建议目录
11
+ * /suggestions install <dedupKey> → 从目录装机 (生成 suggestion 或 cron job)
12
+ *
13
+ * 返回 [text, consumedSuggestionId?]; 无副作用逻辑放在 store, 本模块只做编排与文本渲染.
14
+ */
15
+ import { listSuggestions, acceptSuggestion, dismissSuggestion, clearSuggestions, addSuggestion, } from './suggestions.js';
16
+ import { addJob } from './jobs-store.js';
17
+ import { findCatalogEntry, SUGGESTION_CATALOG } from './suggestion-catalog.js';
18
+ function noArgs() {
19
+ return '用法: /suggestions \u3000| accept <n> \u3000| dismiss <n> \u3000| clear \u3000| catalog \u3000| install <dedupKey>';
20
+ }
21
+ function renderList(items) {
22
+ if (items.length === 0)
23
+ return '当前没有待处理建议。\n\n可用: /suggestions catalog 查看内置建议目录。';
24
+ const lines = items.map((s, i) => `${i + 1}. [${s.source}] ${s.summary}${s.dueAt ? ` (到期 ${s.dueAt})` : ''}`);
25
+ return lines.join('\n') + `\n\n用 /suggestions accept <n> 接受, dismiss <n> 忽略, clear 全清。`;
26
+ }
27
+ export function renderCatalog() {
28
+ return '内置建议目录:\n' +
29
+ SUGGESTION_CATALOG.map((c) => `- ${c.dedupKey}: ${c.title} — ${c.summary}`).join('\n') +
30
+ '\n\n用 /suggestions install <dedupKey> 装机。';
31
+ }
32
+ export async function handleSuggestionsCommand(args, home) {
33
+ const parts = args.trim().split(/\s+/).filter(Boolean);
34
+ const action = parts[0]?.toLowerCase() ?? '';
35
+ switch (action) {
36
+ case '':
37
+ case 'list': {
38
+ const items = await listSuggestions(home);
39
+ return { text: renderList(items) };
40
+ }
41
+ case 'accept': {
42
+ const n = Number(parts[1]);
43
+ if (!Number.isInteger(n) || n < 1)
44
+ return { text: '用法: /suggestions accept <n>' };
45
+ const items = await listSuggestions(home);
46
+ const target = items[n - 1];
47
+ if (!target)
48
+ return { text: `没有第 ${n} 条建议。` };
49
+ const accepted = await acceptSuggestion(target.id, home);
50
+ if (!accepted)
51
+ return { text: '该建议已被移除, 请重试。' };
52
+ return {
53
+ text: `已接受: ${accepted.summary}`,
54
+ accepted,
55
+ };
56
+ }
57
+ case 'dismiss': {
58
+ const n = Number(parts[1]);
59
+ if (!Number.isInteger(n) || n < 1)
60
+ return { text: '用法: /suggestions dismiss <n>' };
61
+ const items = await listSuggestions(home);
62
+ const target = items[n - 1];
63
+ if (!target)
64
+ return { text: `没有第 ${n} 条建议。` };
65
+ const ok = await dismissSuggestion(target.id, home);
66
+ return { text: ok ? `已忽略: ${target.summary}` : '忽略失败。' };
67
+ }
68
+ case 'clear': {
69
+ const n = await clearSuggestions(home);
70
+ return { text: `已清空 ${n} 条建议。` };
71
+ }
72
+ case 'catalog': {
73
+ return {
74
+ text: renderCatalog(),
75
+ };
76
+ }
77
+ case 'install': {
78
+ const key = parts[1];
79
+ if (!key)
80
+ return { text: '用法: /suggestions install <dedupKey>' };
81
+ const entry = findCatalogEntry(key);
82
+ if (!entry)
83
+ return { text: `目录里没有 ${key}。用 /suggestions catalog 看。` };
84
+ // 有 schedule + prompt → 直接生成 cron job; 否则先塞一条建议
85
+ const s = await addSuggestion({ dedupKey: entry.dedupKey, summary: entry.summary, source: 'system' }, home);
86
+ let extra = '';
87
+ if (entry.schedule && entry.prompt) {
88
+ await addJob({ name: entry.title, schedule: entry.schedule, prompt: entry.prompt }, home);
89
+ extra = ` 并生成了定时任务 (${entry.schedule})。`;
90
+ }
91
+ return { text: `已装机: ${entry.title}${extra}\n(可用 /suggestions dismiss 1 撤销这条建议)` };
92
+ }
93
+ default:
94
+ return { text: noArgs() };
95
+ }
96
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * suggestions.ts — 轻量建议队列 (sticky-note suggestions)
3
+ *
4
+ * 借鉴 Hermes agent cron/suggestions.py + suggestion_catalog.py 心智:
5
+ * - 建议是一个个"待办/提醒/下一动作", 由调度器/策略/系统在合适的时机塞入
6
+ * - 同一 dedup_key 只保留最新的一个 (避免同一提醒重复刷屏)
7
+ * - MAX_PENDING 上限, 超出自动丢最旧的 (队列有界, 不无限膨胀)
8
+ * - 用户可接受 (accept) / 忽略 (dismiss) / 全部清空 (clear)
9
+ * - 每个 suggestion 记住来源 (source) / 摘要 (summary) / 到期 (dueAt?, 可选)
10
+ * - 落盘 ~/.bolloon/suggestions.json, 进程内互斥链序列化 (对齐 server-storage)
11
+ */
12
+ import * as fs from 'fs/promises';
13
+ import * as os from 'os';
14
+ import * as path from 'path';
15
+ import * as crypto from 'crypto';
16
+ export const MAX_PENDING = 5; // 借鉴 hermes: 建议队列有界, 最长该数
17
+ function getSuggestionsPath(home = os.homedir()) {
18
+ return path.join(home, '.bolloon', 'suggestions.json');
19
+ }
20
+ // 进程内互斥链: 所有写操作串行, 失败不毒化链 (对齐 server-storage 的 updateChannels 设计)
21
+ let suggestionsLock = Promise.resolve();
22
+ function withLock(fn) {
23
+ const run = suggestionsLock.then(fn);
24
+ suggestionsLock = run.then(() => undefined, () => undefined);
25
+ return run;
26
+ }
27
+ async function readAll(home) {
28
+ try {
29
+ const raw = await fs.readFile(getSuggestionsPath(home), 'utf-8');
30
+ const parsed = JSON.parse(raw);
31
+ if (parsed?.version === 1 && Array.isArray(parsed.items))
32
+ return parsed;
33
+ return { version: 1, items: [] };
34
+ }
35
+ catch {
36
+ return { version: 1, items: [] };
37
+ }
38
+ }
39
+ async function writeAll(file, home) {
40
+ const p = getSuggestionsPath(home);
41
+ await fs.mkdir(path.dirname(p), { recursive: true });
42
+ // 原子写: 先写临时文件再 rename, 避免半截 JSON (对齐 plan-store 直接重写该校验线)
43
+ const tmp = p + '.tmp';
44
+ await fs.writeFile(tmp, JSON.stringify(file, null, 2), 'utf-8');
45
+ await fs.rename(tmp, p);
46
+ }
47
+ /**
48
+ * 添加一条建议. 同一 dedupKey 已存在 → 更新其 summary/来源, 并重置插入到队尾; 不超过 MAX_PENDING.
49
+ * @returns 新构建 (或更新后) 的 suggestion
50
+ */
51
+ export async function addSuggestion(input, home = os.homedir()) {
52
+ return withLock(async () => {
53
+ const file = await readAll(home);
54
+ const existing = file.items.find((s) => s.dedupKey === input.dedupKey);
55
+ const now = new Date().toISOString();
56
+ const entry = {
57
+ id: existing?.id ?? crypto.randomUUID(),
58
+ dedupKey: input.dedupKey,
59
+ summary: input.summary,
60
+ source: input.source,
61
+ relatedTaskId: input.relatedTaskId,
62
+ createdAt: existing?.createdAt ?? now,
63
+ ...(input.dueAt ? { dueAt: input.dueAt } : {}),
64
+ };
65
+ file.items = file.items.filter((s) => s.dedupKey !== input.dedupKey);
66
+ file.items.push(entry);
67
+ while (file.items.length > MAX_PENDING)
68
+ file.items.shift(); // 丢最旧的
69
+ await writeAll(file, home);
70
+ return entry;
71
+ });
72
+ }
73
+ /** 全部未完成建议 */
74
+ export async function listSuggestions(home = os.homedir()) {
75
+ return withLock(async () => (await readAll(home)).items);
76
+ }
77
+ /** 接受: 从队列移除并返回它 (调用方可据此做后续动作, 如转成 task) */
78
+ export async function acceptSuggestion(id, home = os.homedir()) {
79
+ return withLock(async () => {
80
+ const file = await readAll(home);
81
+ const idx = file.items.findIndex((s) => s.id === id);
82
+ if (idx < 0)
83
+ return null;
84
+ const [removed] = file.items.splice(idx, 1);
85
+ await writeAll(file, home);
86
+ return removed;
87
+ });
88
+ }
89
+ /** 忽略: 从队列移除 */
90
+ export async function dismissSuggestion(id, home = os.homedir()) {
91
+ return withLock(async () => {
92
+ const file = await readAll(home);
93
+ const before = file.items.length;
94
+ file.items = file.items.filter((s) => s.id !== id);
95
+ if (file.items.length === before)
96
+ return false;
97
+ await writeAll(file, home);
98
+ return true;
99
+ });
100
+ }
101
+ /** 全部清空 */
102
+ export async function clearSuggestions(home = os.homedir()) {
103
+ return withLock(async () => {
104
+ const file = await readAll(home);
105
+ const n = file.items.length;
106
+ file.items = [];
107
+ await writeAll(file, home);
108
+ return n;
109
+ });
110
+ }
@@ -9,60 +9,60 @@ import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import { firstRunFlagPath, dataDir, logsDir } from './paths';
11
11
  import { log } from './logger';
12
- const OVERLAY_HTML = `
13
- <!DOCTYPE html>
14
- <html>
15
- <head>
16
- <meta charset="utf-8" />
17
- <style>
18
- :root { color-scheme: light dark; }
19
- body {
20
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
- margin: 0; padding: 32px 28px;
22
- background: rgba(245,245,247,0.97);
23
- color: #1d1d1f;
24
- height: 100vh; box-sizing: border-box;
25
- }
26
- @media (prefers-color-scheme: dark) {
27
- body { background: rgba(28,28,30,0.97); color: #f5f5f7; }
28
- }
29
- h1 { font-size: 18px; margin: 0 0 14px; font-weight: 600; }
30
- p { font-size: 13px; line-height: 1.55; margin: 8px 0; }
31
- code {
32
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
33
- font-size: 12px; padding: 1px 5px;
34
- background: rgba(0,0,0,0.06); border-radius: 3px;
35
- }
36
- @media (prefers-color-scheme: dark) {
37
- code { background: rgba(255,255,255,0.08); }
38
- }
39
- .btn {
40
- display: inline-block; margin-top: 18px; padding: 8px 18px;
41
- background: #007aff; color: #fff; border: none; border-radius: 6px;
42
- font-size: 13px; font-weight: 500; cursor: pointer;
43
- }
44
- .btn:hover { background: #0066d6; }
45
- ul { padding-left: 20px; }
46
- li { font-size: 13px; line-height: 1.7; }
47
- </style>
48
- </head>
49
- <body>
50
- <h1>欢迎使用 Bolloon Agent</h1>
51
- <p>你的数据存放在本地, 不上传:</p>
52
- <ul>
53
- <li>配置 / 频道 / P2P 密钥: <code id="data"></code></li>
54
- <li>运行日志: <code id="logs"></code></li>
55
- </ul>
56
- <p>所有数据在卸载后仍保留 — 想清理就手动删除 ~/.bolloon/。</p>
57
- <button class="btn" id="ok">知道了</button>
58
- <script>
59
- const ok = document.getElementById('ok');
60
- ok.addEventListener('click', () => window.electronAPI.markFirstRunSeen());
61
- document.getElementById('data').textContent = window.electronAPI.getDataPathSync?.() || '';
62
- document.getElementById('logs').textContent = window.electronAPI.getLogsPathSync?.() || '';
63
- </script>
64
- </body>
65
- </html>
12
+ const OVERLAY_HTML = `
13
+ <!DOCTYPE html>
14
+ <html>
15
+ <head>
16
+ <meta charset="utf-8" />
17
+ <style>
18
+ :root { color-scheme: light dark; }
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
+ margin: 0; padding: 32px 28px;
22
+ background: rgba(245,245,247,0.97);
23
+ color: #1d1d1f;
24
+ height: 100vh; box-sizing: border-box;
25
+ }
26
+ @media (prefers-color-scheme: dark) {
27
+ body { background: rgba(28,28,30,0.97); color: #f5f5f7; }
28
+ }
29
+ h1 { font-size: 18px; margin: 0 0 14px; font-weight: 600; }
30
+ p { font-size: 13px; line-height: 1.55; margin: 8px 0; }
31
+ code {
32
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
33
+ font-size: 12px; padding: 1px 5px;
34
+ background: rgba(0,0,0,0.06); border-radius: 3px;
35
+ }
36
+ @media (prefers-color-scheme: dark) {
37
+ code { background: rgba(255,255,255,0.08); }
38
+ }
39
+ .btn {
40
+ display: inline-block; margin-top: 18px; padding: 8px 18px;
41
+ background: #007aff; color: #fff; border: none; border-radius: 6px;
42
+ font-size: 13px; font-weight: 500; cursor: pointer;
43
+ }
44
+ .btn:hover { background: #0066d6; }
45
+ ul { padding-left: 20px; }
46
+ li { font-size: 13px; line-height: 1.7; }
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <h1>欢迎使用 Bolloon Agent</h1>
51
+ <p>你的数据存放在本地, 不上传:</p>
52
+ <ul>
53
+ <li>配置 / 频道 / P2P 密钥: <code id="data"></code></li>
54
+ <li>运行日志: <code id="logs"></code></li>
55
+ </ul>
56
+ <p>所有数据在卸载后仍保留 — 想清理就手动删除 ~/.bolloon/。</p>
57
+ <button class="btn" id="ok">知道了</button>
58
+ <script>
59
+ const ok = document.getElementById('ok');
60
+ ok.addEventListener('click', () => window.electronAPI.markFirstRunSeen());
61
+ document.getElementById('data').textContent = window.electronAPI.getDataPathSync?.() || '';
62
+ document.getElementById('logs').textContent = window.electronAPI.getLogsPathSync?.() || '';
63
+ </script>
64
+ </body>
65
+ </html>
66
66
  `;
67
67
  export function hasSeenFirstRun() {
68
68
  try {