@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21

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 (359) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +8 -11
  3. package/docs/prefix-cache-architecture.md +14 -0
  4. package/package.json +22 -24
  5. package/packages/ask-user/README.md +55 -107
  6. package/packages/ask-user/ask-ui.ts +306 -732
  7. package/packages/ask-user/clipboard.ts +50 -0
  8. package/packages/ask-user/config.ts +74 -78
  9. package/packages/ask-user/package.json +2 -2
  10. package/packages/ask-user/questions.ts +151 -0
  11. package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
  12. package/packages/ask-user/tools.ts +198 -432
  13. package/packages/autocomplete/package.json +1 -1
  14. package/packages/autocomplete/src/constants.ts +38 -43
  15. package/packages/autocomplete/src/sorting.ts +8 -0
  16. package/packages/background-tasks/package.json +2 -2
  17. package/packages/background-tasks/src/config.ts +0 -73
  18. package/packages/background-tasks/src/index.ts +20 -16
  19. package/packages/background-tasks/src/registry.ts +1 -171
  20. package/packages/background-tasks/src/types.ts +0 -32
  21. package/packages/btw/README.md +13 -56
  22. package/packages/btw/extensions/btw.ts +403 -1796
  23. package/packages/btw/package.json +5 -2
  24. package/packages/btw/skills/btw/SKILL.md +7 -143
  25. package/packages/compactor/README.md +56 -287
  26. package/packages/compactor/package.json +5 -9
  27. package/packages/compactor/skills/compactor/SKILL.md +13 -20
  28. package/packages/compactor/src/card.ts +90 -0
  29. package/packages/compactor/src/commands/index.ts +73 -130
  30. package/packages/compactor/src/compaction/compact-args.ts +5 -3
  31. package/packages/compactor/src/compaction/filter-noise.ts +1 -1
  32. package/packages/compactor/src/compaction/hooks.ts +487 -311
  33. package/packages/compactor/src/compaction/source.ts +178 -0
  34. package/packages/compactor/src/compaction/summarize.ts +586 -52
  35. package/packages/compactor/src/config/manager.ts +130 -127
  36. package/packages/compactor/src/config/schema.ts +32 -48
  37. package/packages/compactor/src/index.ts +40 -431
  38. package/packages/compactor/src/info-screen.ts +44 -190
  39. package/packages/compactor/src/session/recall-blocks.ts +1 -1
  40. package/packages/compactor/src/stats.ts +76 -0
  41. package/packages/compactor/src/tools/context-budget.ts +17 -34
  42. package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
  43. package/packages/compactor/src/tools/register.ts +29 -269
  44. package/packages/compactor/src/types.ts +47 -186
  45. package/packages/core/README.md +25 -0
  46. package/packages/core/child-agent.ts +1 -0
  47. package/packages/core/command-echo.ts +63 -0
  48. package/packages/core/compaction-context.ts +41 -0
  49. package/packages/core/constants.ts +8 -38
  50. package/packages/core/events.ts +0 -53
  51. package/packages/core/fusion-status.ts +4 -0
  52. package/packages/core/global.d.ts +10 -0
  53. package/packages/core/harness-messages.ts +392 -0
  54. package/packages/core/index.ts +14 -0
  55. package/packages/core/kanboard-status.ts +49 -0
  56. package/packages/core/long-horizon-owner-status.ts +46 -0
  57. package/packages/core/model-cache.ts +81 -7
  58. package/packages/core/package.json +6 -3
  59. package/packages/core/src/attach/detect.ts +163 -0
  60. package/packages/core/src/child-agent/child-args.ts +31 -0
  61. package/packages/core/src/child-agent/delivery.ts +41 -0
  62. package/packages/core/src/child-agent/index.ts +5 -0
  63. package/packages/core/src/child-agent/readers.ts +203 -0
  64. package/packages/core/src/child-agent/runtime.ts +584 -0
  65. package/packages/core/src/evidence.ts +78 -0
  66. package/packages/core/src/hints/browser.ts +278 -0
  67. package/packages/core/src/hints/crab-data.ts +19 -0
  68. package/packages/core/src/hints/index.ts +865 -0
  69. package/packages/core/src/hints/lines.ts +607 -0
  70. package/packages/core/src/hints/store.ts +120 -0
  71. package/packages/core/src/jev/chatter.ts +24 -0
  72. package/packages/core/src/jev/settings.ts +177 -14
  73. package/packages/core/src/package-colors.ts +2 -0
  74. package/packages/core/src/settings/catalog.ts +39 -15
  75. package/packages/core/src/settings/engine.ts +48 -2
  76. package/packages/core/src/settings/hub.ts +83 -20
  77. package/packages/core/src/settings/open.ts +49 -0
  78. package/packages/core/src/settings/schema.ts +27 -7
  79. package/packages/core/src/tui/hub-kit.ts +18 -5
  80. package/packages/core/src/tui/kit.ts +198 -0
  81. package/packages/core/src/tui/progress.ts +73 -0
  82. package/packages/core/src/turn/arbiter.ts +290 -0
  83. package/packages/footer/README.md +4 -6
  84. package/packages/footer/package.json +3 -3
  85. package/packages/footer/src/glance-editor.ts +19 -11
  86. package/packages/footer/src/help.ts +1 -1
  87. package/packages/footer/src/index.ts +36 -3
  88. package/packages/footer/src/presets.ts +3 -4
  89. package/packages/footer/src/rendering/icons.ts +0 -20
  90. package/packages/footer/src/segments/compactor.ts +52 -142
  91. package/packages/footer/src/segments/status-ext.ts +0 -1
  92. package/packages/fusion/README.md +14 -4
  93. package/packages/fusion/package.json +4 -3
  94. package/packages/fusion/src/child-args.ts +1 -0
  95. package/packages/fusion/src/index.ts +161 -8
  96. package/packages/fusion/src/pi-settings.ts +57 -0
  97. package/packages/fusion/src/picker.ts +186 -51
  98. package/packages/fusion/src/preset.ts +56 -2
  99. package/packages/fusion/src/prompts.ts +1 -1
  100. package/packages/fusion/src/sidekick-runtime.ts +1 -420
  101. package/packages/fusion/src/tools.ts +107 -128
  102. package/packages/fusion/src/transcript.ts +27 -85
  103. package/packages/info-screen/package.json +2 -2
  104. package/packages/input-shortcuts/README.md +18 -30
  105. package/packages/input-shortcuts/package.json +2 -2
  106. package/packages/input-shortcuts/src/burst.ts +88 -0
  107. package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
  108. package/packages/input-shortcuts/src/cleared-input.ts +97 -0
  109. package/packages/input-shortcuts/src/index.ts +99 -193
  110. package/packages/input-shortcuts/src/last-response.ts +40 -0
  111. package/packages/input-shortcuts/src/registers.ts +8 -23
  112. package/packages/input-shortcuts/src/types.ts +4 -16
  113. package/packages/input-shortcuts/src/undo-redo.ts +59 -47
  114. package/packages/kanboard/README.md +51 -47
  115. package/packages/kanboard/index.ts +293 -55
  116. package/packages/kanboard/package.json +8 -8
  117. package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
  118. package/packages/kanboard/src/badges.ts +93 -0
  119. package/packages/kanboard/src/bin.ts +4 -2
  120. package/packages/kanboard/src/commands.ts +218 -84
  121. package/packages/kanboard/src/debug.ts +20 -0
  122. package/packages/kanboard/src/guard.ts +267 -109
  123. package/packages/kanboard/src/monitor.ts +330 -0
  124. package/packages/kanboard/src/notice-buffer.ts +74 -0
  125. package/packages/kanboard/src/progress.ts +27 -0
  126. package/packages/kanboard/src/reminders.ts +234 -0
  127. package/packages/kanboard/src/settings.ts +69 -15
  128. package/packages/kanboard/src/shapes.ts +5 -0
  129. package/packages/long-horizon/index.ts +190 -13
  130. package/packages/long-horizon/package.json +3 -3
  131. package/packages/long-horizon/src/commands.ts +103 -7
  132. package/packages/long-horizon/src/compaction-brief.ts +35 -0
  133. package/packages/long-horizon/src/engine/continuation.ts +84 -15
  134. package/packages/long-horizon/src/engine/goal-state.ts +64 -13
  135. package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
  136. package/packages/long-horizon/src/engine/ralph.ts +3 -2
  137. package/packages/long-horizon/src/engine/runaway.ts +68 -11
  138. package/packages/long-horizon/src/engine/verifier.ts +19 -0
  139. package/packages/long-horizon/src/gate.ts +37 -12
  140. package/packages/long-horizon/src/judge/resolve.ts +1 -0
  141. package/packages/long-horizon/src/owner.ts +35 -2
  142. package/packages/long-horizon/src/progress.ts +87 -0
  143. package/packages/long-horizon/src/prompts/goal.ts +1 -1
  144. package/packages/long-horizon/src/runtime.ts +172 -18
  145. package/packages/long-horizon/src/settings.ts +54 -39
  146. package/packages/long-horizon/src/tools/goal.ts +6 -0
  147. package/packages/long-horizon/src/tools/graph.ts +1 -1
  148. package/packages/long-horizon/src/tools/swarm.ts +3 -3
  149. package/packages/mcp/package.json +2 -2
  150. package/packages/memory/commands.ts +7 -2
  151. package/packages/memory/index.ts +210 -32
  152. package/packages/memory/package.json +3 -3
  153. package/packages/memory/save-session.ts +264 -0
  154. package/packages/memory/settings.ts +17 -0
  155. package/packages/memory/tools.ts +253 -203
  156. package/packages/notify/package.json +2 -2
  157. package/packages/skill-registry/README.md +53 -16
  158. package/packages/skill-registry/index.ts +230 -0
  159. package/packages/skill-registry/package.json +19 -8
  160. package/packages/skill-registry/src/editor.ts +206 -0
  161. package/packages/skill-registry/src/judge.ts +289 -0
  162. package/packages/skill-registry/src/registry.ts +87 -0
  163. package/packages/skill-registry/src/settings.ts +259 -0
  164. package/packages/skill-registry/src/vault.ts +80 -0
  165. package/packages/subagents/README.md +39 -119
  166. package/packages/subagents/package.json +6 -13
  167. package/packages/subagents/skills/subagents/SKILL.md +18 -42
  168. package/packages/subagents/src/agents.ts +213 -0
  169. package/packages/subagents/src/cards.ts +182 -0
  170. package/packages/subagents/src/index.ts +609 -1084
  171. package/packages/subagents/src/manager.ts +429 -0
  172. package/packages/subagents/src/profiles.ts +130 -0
  173. package/packages/subagents/src/transcript.ts +154 -0
  174. package/packages/subagents/src/ui.ts +389 -0
  175. package/packages/unipi/bundled.js +39756 -45569
  176. package/packages/unipi/index.ts +55 -10
  177. package/packages/updater/package.json +2 -2
  178. package/packages/utility/README.md +76 -69
  179. package/packages/utility/package.json +2 -2
  180. package/packages/utility/skills/image/SKILL.md +37 -0
  181. package/packages/utility/skills/utility/SKILL.md +17 -60
  182. package/packages/utility/src/answer/extract.ts +128 -0
  183. package/packages/utility/src/answer/index.ts +213 -0
  184. package/packages/utility/src/answer/reply.ts +133 -0
  185. package/packages/utility/src/answer/web.ts +167 -0
  186. package/packages/utility/src/attach/index.ts +205 -0
  187. package/packages/utility/src/commands.ts +38 -142
  188. package/packages/utility/src/diagnostics/engine.ts +50 -5
  189. package/packages/utility/src/image/index.ts +33 -0
  190. package/packages/utility/src/image/models.ts +221 -0
  191. package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
  192. package/packages/utility/src/image/settings.ts +178 -0
  193. package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
  194. package/packages/utility/src/image/tools.ts +168 -0
  195. package/packages/utility/src/image/transports.ts +186 -0
  196. package/packages/utility/src/index.ts +68 -389
  197. package/packages/utility/src/lifecycle/cleanup.ts +116 -311
  198. package/packages/utility/src/rename/gate.ts +102 -0
  199. package/packages/utility/src/rename/index.ts +191 -0
  200. package/packages/utility/src/rename/session.ts +170 -0
  201. package/packages/utility/src/render/delegated.ts +285 -0
  202. package/packages/utility/src/render/harness.ts +683 -0
  203. package/packages/utility/src/render/parse.ts +156 -0
  204. package/packages/utility/src/render/pi-settings.ts +38 -0
  205. package/packages/utility/src/render/reply-bg.ts +307 -0
  206. package/packages/utility/src/render/simple.ts +724 -0
  207. package/packages/utility/src/render/spacing.ts +235 -0
  208. package/packages/utility/src/render/styled.ts +251 -0
  209. package/packages/utility/src/render/tools.ts +353 -0
  210. package/packages/utility/src/settings.ts +166 -233
  211. package/packages/utility/src/types.ts +1 -222
  212. package/packages/watchdog/index.ts +17 -6
  213. package/packages/watchdog/package.json +3 -3
  214. package/packages/watchdog/src/config.ts +3 -2
  215. package/packages/web-api/package.json +2 -2
  216. package/packages/workflow/README.md +3 -0
  217. package/packages/workflow/index.ts +0 -2
  218. package/packages/workflow/package.json +2 -2
  219. package/packages/workflow/src/permission/decide.ts +6 -4
  220. package/packages/workflow/src/permission/index.ts +17 -7
  221. package/packages/workflow/src/permission/settings.ts +39 -4
  222. package/packages/workflow/src/plan/index.ts +19 -4
  223. package/packages/background-tasks/extensions/delegate-child.ts +0 -1
  224. package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
  225. package/packages/background-tasks/src/context-token-budget.ts +0 -903
  226. package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
  227. package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
  228. package/packages/background-tasks/src/delegate/budget.ts +0 -415
  229. package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
  230. package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
  231. package/packages/background-tasks/src/delegate/launch.ts +0 -484
  232. package/packages/background-tasks/src/delegate/result-package.ts +0 -459
  233. package/packages/background-tasks/src/delegate/runner.ts +0 -430
  234. package/packages/background-tasks/src/delegate/seed.ts +0 -423
  235. package/packages/background-tasks/src/delegate/types.ts +0 -323
  236. package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
  237. package/packages/background-tasks/src/delegate-extension.ts +0 -654
  238. package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
  239. package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
  240. package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
  241. package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
  242. package/packages/compactor/src/compaction/build-sections.ts +0 -76
  243. package/packages/compactor/src/compaction/extract/goals.ts +0 -36
  244. package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
  245. package/packages/compactor/src/compaction/format.ts +0 -53
  246. package/packages/compactor/src/compaction/merge.ts +0 -143
  247. package/packages/compactor/src/config/presets.ts +0 -160
  248. package/packages/compactor/src/executor/executor.ts +0 -233
  249. package/packages/compactor/src/executor/runtime.ts +0 -115
  250. package/packages/compactor/src/security/evaluator.ts +0 -89
  251. package/packages/compactor/src/security/policy.ts +0 -96
  252. package/packages/compactor/src/security/scanner.ts +0 -65
  253. package/packages/compactor/src/session/auto-inject.ts +0 -60
  254. package/packages/compactor/src/session/db.ts +0 -325
  255. package/packages/compactor/src/session/extract.ts +0 -144
  256. package/packages/compactor/src/session/resume-inject.ts +0 -64
  257. package/packages/compactor/src/session/snapshot.ts +0 -327
  258. package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
  259. package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
  260. package/packages/compactor/src/tools/ctx-execute.ts +0 -23
  261. package/packages/compactor/src/tools/ctx-stats.ts +0 -96
  262. package/packages/image/README.md +0 -99
  263. package/packages/image/package.json +0 -61
  264. package/packages/image/skills/image/SKILL.md +0 -76
  265. package/packages/image/src/generate.ts +0 -256
  266. package/packages/image/src/index.ts +0 -135
  267. package/packages/image/src/models.ts +0 -519
  268. package/packages/image/src/openai-images-api.ts +0 -282
  269. package/packages/image/src/register-providers.ts +0 -209
  270. package/packages/image/src/settings.ts +0 -173
  271. package/packages/image/src/tools.ts +0 -326
  272. package/packages/input-shortcuts/src/clipboard.ts +0 -119
  273. package/packages/kanboard/src/runner.ts +0 -817
  274. package/packages/skill-registry/skills/auto/SKILL.md +0 -465
  275. package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
  276. package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
  277. package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
  278. package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
  279. package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
  280. package/packages/skill-registry/skills/debug/SKILL.md +0 -228
  281. package/packages/skill-registry/skills/document/SKILL.md +0 -120
  282. package/packages/skill-registry/skills/fix/SKILL.md +0 -212
  283. package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
  284. package/packages/skill-registry/skills/plan/SKILL.md +0 -231
  285. package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
  286. package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
  287. package/packages/skill-registry/skills/research/SKILL.md +0 -251
  288. package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
  289. package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
  290. package/packages/skill-registry/skills/work/SKILL.md +0 -218
  291. package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
  292. package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
  293. package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
  294. package/packages/subagents/agents/delegate.md +0 -14
  295. package/packages/subagents/agents/oracle.md +0 -78
  296. package/packages/subagents/agents/researcher.md +0 -52
  297. package/packages/subagents/agents/reviewer.md +0 -80
  298. package/packages/subagents/agents/scout.md +0 -50
  299. package/packages/subagents/agents/worker.md +0 -59
  300. package/packages/subagents/prompts/council.md +0 -48
  301. package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
  302. package/packages/subagents/prompts/parallel-cleanup.md +0 -59
  303. package/packages/subagents/prompts/parallel-research.md +0 -50
  304. package/packages/subagents/prompts/parallel-review.md +0 -54
  305. package/packages/subagents/prompts/review-loop.md +0 -43
  306. package/packages/subagents/skills/council-mode/SKILL.md +0 -230
  307. package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
  308. package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
  309. package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
  310. package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
  311. package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
  312. package/packages/subagents/src/acceptance.ts +0 -331
  313. package/packages/subagents/src/agent-manager.ts +0 -405
  314. package/packages/subagents/src/agent-memory.ts +0 -300
  315. package/packages/subagents/src/agent-overrides.ts +0 -183
  316. package/packages/subagents/src/agent-runner.ts +0 -271
  317. package/packages/subagents/src/async-runner.ts +0 -422
  318. package/packages/subagents/src/authority-policy.ts +0 -47
  319. package/packages/subagents/src/budgets.ts +0 -285
  320. package/packages/subagents/src/child-safety.ts +0 -140
  321. package/packages/subagents/src/config.ts +0 -249
  322. package/packages/subagents/src/conversation-viewer.ts +0 -281
  323. package/packages/subagents/src/core-compat.ts +0 -94
  324. package/packages/subagents/src/custom-agents.ts +0 -343
  325. package/packages/subagents/src/file-system-retry.ts +0 -50
  326. package/packages/subagents/src/fleet-data.ts +0 -105
  327. package/packages/subagents/src/fleet-view.ts +0 -322
  328. package/packages/subagents/src/foreground-detach.ts +0 -59
  329. package/packages/subagents/src/fork-context.ts +0 -285
  330. package/packages/subagents/src/global.d.ts +0 -7
  331. package/packages/subagents/src/guide.ts +0 -129
  332. package/packages/subagents/src/mission-state.ts +0 -133
  333. package/packages/subagents/src/mission-store.ts +0 -436
  334. package/packages/subagents/src/output-limits.ts +0 -142
  335. package/packages/subagents/src/parity-types.ts +0 -315
  336. package/packages/subagents/src/pi-args.ts +0 -167
  337. package/packages/subagents/src/result-files.ts +0 -232
  338. package/packages/subagents/src/result-watcher.ts +0 -221
  339. package/packages/subagents/src/retained-children.ts +0 -176
  340. package/packages/subagents/src/run-fanout-budget.ts +0 -356
  341. package/packages/subagents/src/scheduled-runs.ts +0 -349
  342. package/packages/subagents/src/schemas.ts +0 -263
  343. package/packages/subagents/src/slash-commands.ts +0 -80
  344. package/packages/subagents/src/supervisor-channel.ts +0 -249
  345. package/packages/subagents/src/tool-handler.ts +0 -1144
  346. package/packages/subagents/src/types.ts +0 -238
  347. package/packages/subagents/src/widget.ts +0 -457
  348. package/packages/subagents/src/workflow-script.ts +0 -737
  349. package/packages/subagents/src/workflow-worker.ts +0 -384
  350. package/packages/subagents/src/worktree.ts +0 -614
  351. package/packages/utility/src/analytics/collector.ts +0 -293
  352. package/packages/utility/src/info-screen.ts +0 -80
  353. package/packages/utility/src/lifecycle/process.ts +0 -162
  354. package/packages/utility/src/prefix-cache.ts +0 -263
  355. package/packages/utility/src/skill-discovery.ts +0 -542
  356. package/packages/utility/src/tools/env.ts +0 -113
  357. package/packages/utility/src/tui/name-badge-state.ts +0 -315
  358. package/packages/utility/src/tui/name-badge.ts +0 -109
  359. /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @pi-unipi/utility — image generation transports
3
+ *
4
+ * Three wire formats, one result shape. Every function THROWS on failure with
5
+ * a message worth showing the agent (pi only marks a tool result as failed
6
+ * when execute() throws).
7
+ *
8
+ * openrouter POST {base}/chat/completions, `modalities`, images come back
9
+ * as data URLs in choices[0].message.images
10
+ * fal POST https://fal.run/<model>, `sync_mode` → data URIs;
11
+ * edits send `image_urls`
12
+ * openai-images POST {base}/images/generations; edits send an `image` array
13
+ * (the shape OpenAI, OmniRoute and most gateways accept)
14
+ */
15
+
16
+ import type { ImageApi } from "./settings.js";
17
+
18
+ export interface ImagePayload {
19
+ data: string;
20
+ mimeType: string;
21
+ }
22
+
23
+ export interface GenerateRequest {
24
+ api: ImageApi;
25
+ /** Endpoint base (ignored for fal, which has one host). */
26
+ baseUrl: string;
27
+ apiKey: string;
28
+ modelId: string;
29
+ prompt: string;
30
+ /** Source images → edit. */
31
+ images?: ImagePayload[];
32
+ /** Output modalities for OpenRouter (image-only models reject "text"). */
33
+ output?: string[];
34
+ signal?: AbortSignal;
35
+ fetchImpl?: typeof fetch;
36
+ timeoutMs?: number;
37
+ }
38
+
39
+ export interface GenerateResponse {
40
+ images: ImagePayload[];
41
+ text: string;
42
+ }
43
+
44
+ export const OPENROUTER_BASE = "https://openrouter.ai/api/v1";
45
+ export const FAL_BASE = "https://fal.run";
46
+ const DEFAULT_TIMEOUT_MS = 240_000;
47
+
48
+ const dataUrl = (img: ImagePayload) => `data:${img.mimeType};base64,${img.data}`;
49
+
50
+ /** `data:image/png;base64,....` → payload; undefined for anything else. */
51
+ export function parseDataUrl(url: string): ImagePayload | undefined {
52
+ const m = /^data:([^;,]+)(?:;[^,]*)*,(.*)$/s.exec(url);
53
+ return m && m[2] ? { mimeType: m[1] || "image/png", data: m[2] } : undefined;
54
+ }
55
+
56
+ async function readError(res: Response): Promise<string> {
57
+ const body = await res.text().catch(() => "");
58
+ let detail = body.slice(0, 400);
59
+ try {
60
+ const parsed = JSON.parse(body) as { error?: { message?: string } | string; detail?: unknown; message?: string };
61
+ if (typeof parsed.error === "string") detail = parsed.error;
62
+ else if (parsed.error?.message) detail = parsed.error.message;
63
+ else if (typeof parsed.message === "string") detail = parsed.message;
64
+ else if (parsed.detail) detail = typeof parsed.detail === "string" ? parsed.detail : JSON.stringify(parsed.detail).slice(0, 400);
65
+ } catch {
66
+ // not JSON
67
+ }
68
+ return `${res.status} ${res.statusText}${detail ? `: ${detail}` : ""}`;
69
+ }
70
+
71
+ /** Fetch a remote image (fal without sync_mode, gateways returning URLs). */
72
+ async function download(url: string, fetchImpl: typeof fetch, signal: AbortSignal): Promise<ImagePayload> {
73
+ const inline = parseDataUrl(url);
74
+ if (inline) return inline;
75
+ const res = await fetchImpl(url, { signal });
76
+ if (!res.ok) throw new Error(`Could not download the generated image: ${res.status}`);
77
+ const mimeType = res.headers.get("content-type")?.split(";")[0] || "image/png";
78
+ return { data: Buffer.from(await res.arrayBuffer()).toString("base64"), mimeType };
79
+ }
80
+
81
+ function join(base: string, suffix: string): string {
82
+ return `${base.replace(/\/+$/, "")}/${suffix.replace(/^\/+/, "")}`;
83
+ }
84
+
85
+ async function withTimeout<T>(req: GenerateRequest, run: (signal: AbortSignal, f: typeof fetch) => Promise<T>): Promise<T> {
86
+ const controller = new AbortController();
87
+ const timer = setTimeout(() => controller.abort(), req.timeoutMs ?? DEFAULT_TIMEOUT_MS);
88
+ const onAbort = () => controller.abort();
89
+ req.signal?.addEventListener("abort", onAbort, { once: true });
90
+ try {
91
+ return await run(controller.signal, req.fetchImpl ?? fetch);
92
+ } catch (error) {
93
+ if (req.signal?.aborted) throw new Error("Image request was cancelled.");
94
+ if (controller.signal.aborted) throw new Error("Image request timed out.");
95
+ throw error;
96
+ } finally {
97
+ clearTimeout(timer);
98
+ req.signal?.removeEventListener("abort", onAbort);
99
+ }
100
+ }
101
+
102
+ async function viaOpenRouter(req: GenerateRequest): Promise<GenerateResponse> {
103
+ return withTimeout(req, async (signal, f) => {
104
+ const content: unknown[] = [{ type: "text", text: req.prompt }];
105
+ for (const img of req.images ?? []) content.push({ type: "image_url", image_url: { url: dataUrl(img) } });
106
+ const output = req.output?.includes("image") ? req.output : ["image"];
107
+ const res = await f(join(req.baseUrl || OPENROUTER_BASE, "chat/completions"), {
108
+ method: "POST",
109
+ headers: { Authorization: `Bearer ${req.apiKey}`, "Content-Type": "application/json" },
110
+ body: JSON.stringify({ model: req.modelId, messages: [{ role: "user", content }], modalities: output }),
111
+ signal,
112
+ });
113
+ if (!res.ok) throw new Error(await readError(res));
114
+ const json = (await res.json()) as {
115
+ error?: { message?: string };
116
+ choices?: Array<{ message?: { content?: unknown; images?: Array<{ image_url?: { url?: string } }> } }>;
117
+ };
118
+ if (json.error?.message) throw new Error(json.error.message);
119
+ const message = json.choices?.[0]?.message;
120
+ const images: ImagePayload[] = [];
121
+ for (const item of message?.images ?? []) {
122
+ const url = item.image_url?.url;
123
+ if (url) images.push(await download(url, f, signal));
124
+ }
125
+ const text = typeof message?.content === "string" ? message.content.trim() : "";
126
+ return { images, text };
127
+ });
128
+ }
129
+
130
+ async function viaFal(req: GenerateRequest): Promise<GenerateResponse> {
131
+ return withTimeout(req, async (signal, f) => {
132
+ const body: Record<string, unknown> = { prompt: req.prompt, sync_mode: true, num_images: 1 };
133
+ if (req.images?.length) body.image_urls = req.images.map(dataUrl);
134
+ const res = await f(join(req.baseUrl || FAL_BASE, req.modelId), {
135
+ method: "POST",
136
+ headers: { Authorization: `Key ${req.apiKey}`, "Content-Type": "application/json" },
137
+ body: JSON.stringify(body),
138
+ signal,
139
+ });
140
+ if (!res.ok) throw new Error(await readError(res));
141
+ const json = (await res.json()) as { images?: Array<{ url?: string }>; image?: { url?: string }; description?: string };
142
+ const images: ImagePayload[] = [];
143
+ for (const item of json.images ?? (json.image ? [json.image] : [])) {
144
+ if (item.url) images.push(await download(item.url, f, signal));
145
+ }
146
+ return { images, text: typeof json.description === "string" ? json.description : "" };
147
+ });
148
+ }
149
+
150
+ async function viaOpenAIImages(req: GenerateRequest): Promise<GenerateResponse> {
151
+ if (!req.baseUrl) throw new Error(`No endpoint for ${req.modelId}: set a custom endpoint in /unipi:settings → Image.`);
152
+ return withTimeout(req, async (signal, f) => {
153
+ const body: Record<string, unknown> = { model: req.modelId, prompt: req.prompt };
154
+ if (req.images?.length) body.image = req.images.map(dataUrl);
155
+ const res = await f(join(req.baseUrl, "images/generations"), {
156
+ method: "POST",
157
+ headers: { Authorization: `Bearer ${req.apiKey}`, "Content-Type": "application/json" },
158
+ body: JSON.stringify(body),
159
+ signal,
160
+ });
161
+ if (!res.ok) throw new Error(await readError(res));
162
+ const json = (await res.json()) as { error?: { message?: string }; data?: Array<{ b64_json?: string; url?: string; media_type?: string; mime_type?: string; revised_prompt?: string }> };
163
+ if (json.error?.message) throw new Error(json.error.message);
164
+ const images: ImagePayload[] = [];
165
+ const notes: string[] = [];
166
+ for (const item of json.data ?? []) {
167
+ const declared = item.media_type ?? item.mime_type;
168
+ if (item.b64_json) images.push({ data: item.b64_json, mimeType: declared ?? "image/png" });
169
+ else if (item.url) images.push(await download(item.url, f, signal));
170
+ if (item.revised_prompt && item.revised_prompt !== req.prompt) notes.push(`Revised prompt: ${item.revised_prompt}`);
171
+ }
172
+ return { images, text: notes.join("\n") };
173
+ });
174
+ }
175
+
176
+ /** Run one generation/edit; throws unless at least one image came back. */
177
+ export async function generate(req: GenerateRequest): Promise<GenerateResponse> {
178
+ if (!req.prompt.trim()) throw new Error("A non-empty prompt is required.");
179
+ if (!req.apiKey) throw new Error("No API key for this image model.");
180
+ const run = req.api === "fal" ? viaFal : req.api === "openrouter" ? viaOpenRouter : viaOpenAIImages;
181
+ const out = await run(req);
182
+ if (out.images.length === 0) {
183
+ throw new Error(out.text ? `The model returned no image. It said: ${out.text.slice(0, 300)}` : "The model returned no image.");
184
+ }
185
+ return out;
186
+ }
@@ -1,423 +1,102 @@
1
1
  /**
2
2
  * @pi-unipi/utility — Extension entry
3
3
  *
4
- * Comprehensive utilities suite for Pi coding agent:
5
- * - Commands: continue, reload, status, cleanup, env, doctor, badge
6
- * - Tools: ctx_env, set_session_name
7
- * - Lifecycle: process management, stale cleanup
8
- * - Analytics: lightweight event collection
9
- * - Diagnostics: cross-module health checks
10
- * - Prefix-cache observability: privacy-safe provider cache stats
11
- * - TUI: util settings, name badge (incl. Herdr pane title sync)
4
+ * - /unipi:settings — the unified settings hub
5
+ * - /unipi:continue (/unipi:retry), /unipi:cleanup, /unipi:doctor, /unipi:answer
6
+ * - Automatic session naming (jev gate + isolated one-tool session) + Herdr sync
7
+ * - Pasted images/files → [Image #N] / [File #N] attachments
8
+ * - Image tools: image_generate, image_edit, image_recognize
9
+ * - Response formatting: simple | regular | advanced tool rendering
10
+ * - The shared model cache (~/.unipi/config/models-cache.json)
12
11
  */
13
12
 
14
13
  import { dirname } from "node:path";
15
14
  import { fileURLToPath } from "node:url";
16
- import type { ExtensionAPI, ExtensionCommandContext, InputEvent, AgentEndEvent } from "@earendil-works/pi-coding-agent";
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
16
  import {
18
17
  UNIPI_EVENTS,
19
18
  MODULES,
20
19
  UTILITY_COMMANDS,
21
- UTILITY_TOOLS,
22
20
  emitEvent,
23
21
  getPackageVersion,
24
- runCommandByName,
25
- registerCommandRunner,
26
- SettingsHub,
27
- HUB_OVERLAY_OPTIONS,
28
- type UnipiBadgeGenerateRequestEvent,
22
+ installHints,
23
+ openSettingsHub,
24
+ chatModelsToCache,
25
+ writeModelCache,
29
26
  } from "@pi-unipi/core";
30
27
  import { registerUtilityCommands } from "./commands.js";
31
- import { registerSkillJudging } from "./skill-discovery.js";
32
- import { NameBadgeState } from "./tui/name-badge-state.js";
33
- import { readBadgeSettings, readUtilSettings } from "./settings.js";
34
- import { getLifecycle } from "./lifecycle/process.js";
35
- import { getAnalyticsCollector } from "./analytics/collector.js";
36
- import { registerInfoScreen } from "./info-screen.js";
37
- import { PrefixCacheTracker, formatPrefixCacheStats } from "./prefix-cache.js";
28
+ import { registerAutoRename } from "./rename/index.js";
29
+ import { registerAnswerCommand } from "./answer/index.js";
30
+ import { registerAttachments } from "./attach/index.js";
31
+ import { imageCatalogEntries, loadImageConfig, refreshImageModelCache, registerImage } from "./image/index.js";
32
+ import { registerToolRenderers } from "./render/tools.js";
33
+ import { installTranscriptSpacing } from "./render/spacing.js";
34
+ import { installHarnessRenderers, installHarnessUserRendering, withHarnessToolAnnotations } from "./render/harness.js";
35
+ import { installHarnessProvenance } from "@pi-unipi/core";
36
+ import { readUtilSettings } from "./settings.js";
37
+
38
+ export { readUtilSettings } from "./settings.js";
39
+ export { simpleWrapTool, simpleWrapped, installSimpleGroupEvents } from "./render/simple.js";
40
+ export { withHarnessToolAnnotations } from "./render/harness.js";
38
41
 
39
- /** Re-export readBadgeSettings for cross-package use */
40
- export { readBadgeSettings } from "./settings.js";
41
-
42
- /** Package version */
43
42
  const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
44
43
 
45
- /** Whether we've seen the first user message (for auto badge generation) */
46
- let firstMessageSeen = false;
47
-
48
- /** Stored user text from first input, used to build conversation summary after agent responds */
49
- let firstUserText = "";
50
-
51
- /** Stored UI context from first input, used to show badge overlay after agent responds */
52
- let firstInputCtx: import("@earendil-works/pi-coding-agent").ExtensionContext | null = null;
53
-
54
- /** All commands registered by this module */
55
44
  const ALL_COMMANDS = [
56
45
  UTILITY_COMMANDS.CONTINUE,
57
- UTILITY_COMMANDS.RELOAD,
58
- UTILITY_COMMANDS.STATUS,
46
+ UTILITY_COMMANDS.RETRY,
59
47
  UTILITY_COMMANDS.CLEANUP,
60
- UTILITY_COMMANDS.ENV,
61
48
  UTILITY_COMMANDS.DOCTOR,
62
- UTILITY_COMMANDS.PREFIX_CACHE,
49
+ UTILITY_COMMANDS.ANSWER,
50
+ "settings",
63
51
  ].map((cmd) => `unipi:${cmd}`);
64
52
 
65
- /** All tools registered by this module */
66
- const ALL_TOOLS = [UTILITY_TOOLS.BATCH, UTILITY_TOOLS.ENV, UTILITY_TOOLS.SET_SESSION_NAME];
67
-
68
53
  export default function (pi: ExtensionAPI) {
69
- // Initialize lifecycle manager
70
- const lifecycle = getLifecycle();
71
-
72
- // Initialize analytics collector
73
- const analytics = getAnalyticsCollector();
74
-
75
- // Session-local provider prefix observability. The random HMAC key and all
76
- // payload-derived fingerprints remain in memory and are discarded on reload.
77
- const prefixCache = new PrefixCacheTracker();
78
- pi.registerCommand(`unipi:${UTILITY_COMMANDS.PREFIX_CACHE}`, {
79
- description: "Show privacy-safe provider prefix-cache diagnostics",
80
- handler: async (_args, ctx) => {
81
- const report = formatPrefixCacheStats(prefixCache.getSnapshot());
82
- if (ctx.hasUI) {
83
- ctx.ui.notify(report, "info");
84
- } else {
85
- pi.sendMessage({ customType: "unipi-response", content: report, display: true }, { deliverAs: "followUp" });
86
- }
87
- },
88
- });
89
-
90
- // Badge actions — "Set session name…" / "Generate session name" rows in
91
- // /unipi:settings (Utility group). The old badge slash commands are
92
- // gone; the hub field is the same persisted setting.
93
- registerCommandRunner("unipi:badge-set-name", async (rawCtx: unknown) => {
94
- const ctx = rawCtx as ExtensionCommandContext;
95
- if (!ctx.hasUI) {
96
- ctx.ui.notify("Setting the session name requires an interactive UI.", "warning");
97
- return;
98
- }
99
- const current = nameBadgeState.isVisible() ? undefined : "";
100
- const name = await ctx.ui.input("Session name", current ?? "");
101
- if (!name || !name.trim()) return;
102
- nameBadgeState.setSessionName(pi, name.trim());
103
- ctx.ui.notify(`Session name set to "${name.trim()}"`, "info");
104
- });
105
- registerCommandRunner("unipi:badge-generate", async (rawCtx: unknown) => {
106
- const ctx = rawCtx as ExtensionCommandContext;
107
- if (!ctx.hasUI) {
108
- ctx.ui.notify("Badge generation requires an interactive UI.", "warning");
109
- return;
110
- }
111
- await nameBadgeState.generate(pi, ctx);
112
- ctx.ui.notify("Generating session name...", "info");
113
- });
114
-
115
- // The unified settings hub — every registered module in one panel.
54
+ // Standalone installs need provenance before any command can send.
55
+ try {
56
+ installHarnessProvenance(pi);
57
+ } catch {
58
+ // cosmetic
59
+ }
116
60
  pi.registerCommand("unipi:settings", {
117
- description: "Configure all unipi modules in one panel (global + project scopes)",
118
- handler: async (_args, ctx) => {
119
- if (!ctx.hasUI) throw new Error("/unipi:settings needs the interactive TUI");
120
- await ctx.ui.custom<void>(
121
- (tui, _theme, _keybindings, done) => {
122
- const hub = new SettingsHub({
123
- cwd: ctx.cwd ?? process.cwd(),
124
- onChanged: (namespace) => {
125
- // Preserve the old overlay's live side-effect: the badge reacts
126
- // immediately when "Show name badge" is toggled in the hub.
127
- if (namespace !== "utility" || !ctx.hasUI) return;
128
- try {
129
- if (readUtilSettings().badge.badgeEnabled) void nameBadgeState.show(pi, ctx);
130
- else nameBadgeState.hide();
131
- } catch {
132
- // Best effort — settings UI must never crash the panel.
133
- }
134
- },
135
- runAction: async (command) => {
136
- const ran = await runCommandByName(command, ctx);
137
- if (!ran) ctx.ui.notify(`no handler registered for ${command}`, "warning");
138
- },
139
- });
140
- hub.onClose = () => done();
141
- return {
142
- focused: true,
143
- invalidate: () => hub.invalidate(),
144
- render: (width: number) => hub.render(width),
145
- handleInput: (data: string) => {
146
- hub.handleInput(data);
147
- tui.requestRender();
148
- },
149
- dispose: () => {},
150
- };
151
- },
152
- HUB_OVERLAY_OPTIONS,
153
- ).catch(() => {
154
- // Overlay errors are non-blocking.
155
- });
156
- },
157
- });
158
-
159
- pi.on("session_start", () => {
160
- // A replacement/reloaded session has a distinct provider prefix lineage.
161
- // The extension factory is normally recreated, but reset explicitly so
162
- // custom hosts that reuse one factory cannot leak counts across sessions.
163
- prefixCache.reset();
61
+ description: "Configure all unipi modules in one panel (global + project scopes); /unipi:settings <search> opens it filtered",
62
+ handler: async (args, ctx) => openSettingsHub(ctx, { filter: args.trim() }),
164
63
  });
165
64
 
166
- pi.on("before_provider_request", (event, ctx) => {
167
- prefixCache.observeRequest(event.payload, ctx.model);
168
- // Observe only: never replace or mutate provider payloads.
169
- });
170
-
171
- pi.on("agent_end", (event) => {
172
- prefixCache.observeMessages(event.messages);
173
- });
174
-
175
- pi.on("model_select", (event) => {
176
- if (event.previousModel && (
177
- event.previousModel.provider !== event.model.provider ||
178
- event.previousModel.id !== event.model.id ||
179
- event.previousModel.api !== event.model.api
180
- )) {
181
- prefixCache.markBoundary("envelope_changed");
182
- }
183
- });
184
- pi.on("thinking_level_select", (event) => {
185
- if (event.previousLevel !== event.level) prefixCache.markBoundary("envelope_changed");
186
- });
187
- pi.on("session_compact", () => prefixCache.markBoundary("history_rewritten"));
188
- pi.on("session_tree", () => prefixCache.markBoundary("history_rewritten"));
189
-
190
- // Register cleanup on shutdown
191
- lifecycle.registerCleanup(async () => {
192
- analytics.disable();
193
- });
194
-
195
- // Initialize name badge state
196
- const nameBadgeState = new NameBadgeState();
197
-
198
- // Capture session context for cross-event use (not needed if BADGE_GENERATE_REQUEST removed)
199
-
200
- // Skill startup discovery gate — when disabled (unipi.skills.discovery: false),
201
- // Unipi's bundled skills are removed from the <available_skills> catalog so
202
- // they never populate agent context; the user's own skills (global, project,
203
- // settings-mounted, third-party packages) stay cataloged. /skill:name
204
- // invocation is unaffected either way: pi expands those commands by reading
205
- // SKILL.md directly. Applied consistently per turn, so the provider prefix
206
- // cache stays intact.
207
- // Skill exposure pipeline: bundled strip → jev freeze → recheck reveals.
208
- registerSkillJudging(pi);
209
-
210
- // Register commands
211
65
  registerUtilityCommands(pi);
66
+ registerAutoRename(pi);
67
+ registerAnswerCommand(pi);
68
+ registerAttachments(pi);
69
+ registerImage(pi);
70
+ registerToolRenderers(pi, readUtilSettings().render.style);
71
+ // Normalize blank runs between transcript blocks (all render styles).
72
+ installTranscriptSpacing(pi);
73
+ // Harness provenance: known custom-type panels + native USER card patch.
74
+ installHarnessRenderers(pi);
75
+ installHarnessUserRendering(pi);
76
+ // Unicrab hints: one line above the editor on startup + matching events.
77
+ installHints(pi);
212
78
 
213
- // Register tools
214
- registerUtilityTools(pi, nameBadgeState);
215
-
216
- // Register info-screen group
217
- registerInfoScreen(pi);
218
-
219
- // Session lifecycle — announce module + restore badge
220
79
  pi.on("session_start", async (_event, ctx) => {
221
- emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
222
- name: MODULES.UTILITY,
223
- version: VERSION,
224
- commands: ALL_COMMANDS,
225
- tools: ALL_TOOLS,
226
- });
227
-
228
- analytics.recordModuleLoad(MODULES.UTILITY, VERSION);
229
-
230
- // Restore name badge if it was visible in previous session
231
- await nameBadgeState.restore(pi, ctx);
232
-
233
- // Auto-show badge on session start if enabled in settings and UI is available.
234
- // The badge shows "Set a name" placeholder until a name is generated.
235
- // Previously the badge only showed after the first agent_end, which meant it
236
- // was absent on restart until the user sent a message.
237
- if (ctx?.hasUI && !nameBadgeState.isVisible()) {
238
- const badgeSettings = readBadgeSettings();
239
- if (badgeSettings.badgeEnabled) {
240
- await nameBadgeState.show(pi, ctx);
241
- }
242
- }
243
-
244
- // Write model cache for TUI components
245
- if ((ctx as any).modelRegistry) {
246
- const { writeModelCache } = await import("@pi-unipi/core");
247
- const registry = (ctx as any).modelRegistry;
248
- const models = (registry.getAvailable?.() ?? registry.getAll())
249
- .map((m: any) => ({ provider: m.provider, id: m.id, name: m.name }));
250
- writeModelCache(models);
251
- }
252
- });
253
-
254
- // First-message hook: capture user text for deferred badge generation
255
- pi.on("input", async (_event, ctx) => {
256
- // Only trigger on first user message
257
- if (firstMessageSeen) return;
258
- firstMessageSeen = true;
259
-
260
- // Check if auto generation is enabled
261
- const settings = readBadgeSettings();
262
- if (!settings.autoGen) return;
263
-
264
- // Skip if badge already has a name
265
- const sessionName = pi.getSessionName?.();
266
- if (sessionName) return;
267
-
268
- // Store first message text for later use in agent_end
269
- // Note: InputEvent.text is the documented property; content may exist at runtime
270
- const content = (_event as unknown as Record<string, unknown>).content;
271
- firstUserText = typeof content === "string"
272
- ? content
273
- : Array.isArray(content)
274
- ? (content as Array<Record<string, unknown>>)
275
- .filter((c) => c.type === "text")
276
- .map((c) => String(c.text))
277
- .join(" ")
278
- : "";
279
-
280
- // Store ctx for badge overlay show after agent responds
281
- firstInputCtx = ctx;
282
- });
283
-
284
- // After agent completes first response, generate badge name with full conversation context
285
- pi.on("agent_end", async (event, _ctx) => {
286
- // Only act if we captured a first input and are waiting for badge generation
287
- if (!firstInputCtx) return;
288
- const ctx = firstInputCtx;
289
- firstInputCtx = null; // consume — only trigger once
290
-
291
- // Check if a name was already set (e.g. manually) in the meantime
292
- const sessionName = pi.getSessionName?.();
293
- if (sessionName) return;
294
-
295
- // Show badge overlay if UI available
296
- if (ctx?.hasUI && !nameBadgeState.isVisible()) {
297
- await nameBadgeState.show(pi, ctx);
298
- }
299
-
300
- // Build conversation summary from full message history (user + assistant)
301
- const messages = event.messages;
302
- const summaryParts: string[] = [];
303
-
304
- // Include the user's first message
305
- if (firstUserText) {
306
- summaryParts.push(`User: ${firstUserText}`);
307
- }
308
-
309
- // Include assistant's response text
310
- const assistantMsgs = messages.filter((m) => m.role === "assistant");
311
- for (const msg of assistantMsgs) {
312
- const content = msg.content;
313
- if (Array.isArray(content)) {
314
- const textParts = content
315
- .filter((c): c is { type: "text"; text: string } => "text" in c && c.type === "text")
316
- .map((c) => c.text)
317
- .join(" ");
318
- if (textParts) summaryParts.push(`Assistant: ${textParts}`);
319
- } else if (typeof content === "string" && content) {
320
- summaryParts.push(`Assistant: ${content}`);
321
- }
322
- }
323
-
324
- // Truncate to reasonable size
325
- const conversationSummary = summaryParts.join("\n").slice(0, 800);
326
-
327
- // Emit event for subagents to spawn background agent
328
- emitEvent(pi, UNIPI_EVENTS.BADGE_GENERATE_REQUEST, {
329
- source: "input-hook",
330
- conversationSummary,
331
- });
332
- });
333
-
334
- // Track command usage
335
- pi.on("tool_call", async (event) => {
336
- if (event.toolName.startsWith("unipi:")) {
337
- analytics.recordCommand(event.toolName, MODULES.UTILITY, 0, true);
80
+ try {
81
+ emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
82
+ name: MODULES.UTILITY,
83
+ version: VERSION,
84
+ commands: ALL_COMMANDS,
85
+ tools: [],
86
+ });
87
+ // Refresh the shared model cache: image models first (their real
88
+ // modalities win over the chat registry's text-only view), then pi's
89
+ // live chat registry (models with credentials, else the full registry).
90
+ const registry = ctx.modelRegistry as unknown as { getAvailable?: () => unknown[]; getAll?: () => unknown[] } | undefined;
91
+ const available = registry?.getAvailable?.() ?? [];
92
+ const chat = chatModelsToCache(available.length > 0 ? available : registry?.getAll?.() ?? []);
93
+ const write = () => writeModelCache([...imageCatalogEntries(loadImageConfig(ctx.cwd)), ...chat]);
94
+ if (chat.length > 0) write();
95
+ void refreshImageModelCache().then((changed) => {
96
+ if (changed && chat.length > 0) write();
97
+ });
98
+ } catch {
99
+ // Best effort — never block session start.
338
100
  }
339
101
  });
340
-
341
- // Session shutdown cleanup
342
- pi.on("session_shutdown", async () => {
343
- nameBadgeState.hide();
344
- firstMessageSeen = false;
345
- firstUserText = "";
346
- firstInputCtx = null;
347
- await lifecycle.shutdown("session_shutdown");
348
- });
349
- }
350
-
351
- /**
352
- * Register utility tools.
353
- */
354
- function registerUtilityTools(pi: ExtensionAPI, nameBadgeState: NameBadgeState): void {
355
- // ctx_env — environment info
356
- pi.registerTool({
357
- name: UTILITY_TOOLS.ENV,
358
- label: "Environment Info",
359
- description: "Show environment information: Node version, Pi version, OS, unipi modules, config paths.",
360
- promptSnippet: "Get environment details for debugging.",
361
- parameters: {
362
- type: "object",
363
- properties: {},
364
- },
365
- async execute() {
366
- const { getEnvironmentInfo, formatEnvironmentInfo } = await import("./tools/env.js");
367
- const info = getEnvironmentInfo();
368
- return {
369
- content: [{ type: "text", text: formatEnvironmentInfo(info) }],
370
- details: info,
371
- };
372
- },
373
- });
374
-
375
- // set_session_name — set the session name for badge display
376
- const badgeSettings = readBadgeSettings();
377
- if (badgeSettings.agentTool) {
378
- pi.registerTool({
379
- name: UTILITY_TOOLS.SET_SESSION_NAME,
380
- label: "Set Session Name",
381
- description:
382
- "Set the session name that appears in the badge overlay and session selector. " +
383
- "Use this to give the current session a descriptive title. " +
384
- "Name should be concise (max 5 words recommended).",
385
- promptSnippet: "Set a name/title for the current session.",
386
- parameters: {
387
- type: "object",
388
- properties: {
389
- name: {
390
- type: "string",
391
- description: "The session name to set (max 5 words recommended).",
392
- },
393
- },
394
- required: ["name"],
395
- },
396
- async execute(_toolCallId, params) {
397
- const { name } = params as { name: string };
398
- if (!name || typeof name !== "string") {
399
- return {
400
- content: [{ type: "text", text: "Error: name parameter is required and must be a string." }],
401
- details: undefined,
402
- };
403
- }
404
-
405
- const trimmed = name.trim();
406
- if (trimmed.length === 0) {
407
- return {
408
- content: [{ type: "text", text: "Error: name cannot be empty." }],
409
- details: undefined,
410
- };
411
- }
412
-
413
- // Set the session name
414
- nameBadgeState.setSessionName(pi, trimmed);
415
-
416
- return {
417
- content: [{ type: "text", text: `Session name set to: "${trimmed}"` }],
418
- details: { name: trimmed },
419
- };
420
- },
421
- });
422
- }
423
102
  }