piu-piu-cli 0.1.0 → 0.2.0

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 (180) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -20
  4. package/bin/piu-piu.mjs +23 -7
  5. package/dist/daemon.mjs +148 -13
  6. package/dist/office/assets/index-CPvN_60E.css +1 -0
  7. package/dist/office/assets/index-DUMIjmLF.js +5971 -0
  8. package/dist/office/index.html +3 -3
  9. package/dist/office/models/anthurium_botany_01.glb +0 -0
  10. package/dist/office/models/calathea_orbifolia_01.glb +0 -0
  11. package/dist/office/models/ceramic_vase_01.glb +0 -0
  12. package/dist/office/models/ceramic_vase_02.glb +0 -0
  13. package/dist/office/models/ceramic_vase_03.glb +0 -0
  14. package/dist/office/models/ceramic_vase_04.glb +0 -0
  15. package/dist/office/models/fern_02.glb +0 -0
  16. package/dist/office/models/mid_century_lounge_chair.glb +0 -0
  17. package/dist/office/models/modern_arm_chair_01.glb +0 -0
  18. package/dist/office/models/pachira_aquatica_01.glb +0 -0
  19. package/dist/office/models/potted_plant_01.glb +0 -0
  20. package/dist/office/models/potted_plant_02.glb +0 -0
  21. package/dist/office/models/potted_plant_04.glb +0 -0
  22. package/dist/office/models/standing_picture_frame_01.glb +0 -0
  23. package/dist/office/models/standing_picture_frame_02.glb +0 -0
  24. package/dist/office/people/business_female_01/avatar.fbx +0 -0
  25. package/dist/office/people/business_female_01/body.webp +0 -0
  26. package/dist/office/people/business_female_01/head.webp +0 -0
  27. package/dist/office/people/business_female_01/opacity.webp +0 -0
  28. package/dist/office/people/business_female_01/portrait.webp +0 -0
  29. package/dist/office/people/business_female_02/avatar.fbx +0 -0
  30. package/dist/office/people/business_female_02/body.webp +0 -0
  31. package/dist/office/people/business_female_02/head.webp +0 -0
  32. package/dist/office/people/business_female_02/opacity.webp +0 -0
  33. package/dist/office/people/business_female_02/portrait.webp +0 -0
  34. package/dist/office/people/business_female_03/avatar.fbx +0 -0
  35. package/dist/office/people/business_female_03/body.webp +0 -0
  36. package/dist/office/people/business_female_03/head.webp +0 -0
  37. package/dist/office/people/business_female_03/opacity.webp +0 -0
  38. package/dist/office/people/business_female_03/portrait.webp +0 -0
  39. package/dist/office/people/business_female_04/avatar.fbx +0 -0
  40. package/dist/office/people/business_female_04/body.webp +0 -0
  41. package/dist/office/people/business_female_04/head.webp +0 -0
  42. package/dist/office/people/business_female_04/opacity.webp +0 -0
  43. package/dist/office/people/business_female_04/portrait.webp +0 -0
  44. package/dist/office/people/business_male_01/avatar.fbx +0 -0
  45. package/dist/office/people/business_male_01/body.webp +0 -0
  46. package/dist/office/people/business_male_01/head.webp +0 -0
  47. package/dist/office/people/business_male_01/opacity.webp +0 -0
  48. package/dist/office/people/business_male_01/portrait.webp +0 -0
  49. package/dist/office/people/business_male_02/avatar.fbx +0 -0
  50. package/dist/office/people/business_male_02/body.webp +0 -0
  51. package/dist/office/people/business_male_02/head.webp +0 -0
  52. package/dist/office/people/business_male_02/opacity.webp +0 -0
  53. package/dist/office/people/business_male_02/portrait.webp +0 -0
  54. package/dist/office/people/business_male_03/avatar.fbx +0 -0
  55. package/dist/office/people/business_male_03/body.webp +0 -0
  56. package/dist/office/people/business_male_03/head.webp +0 -0
  57. package/dist/office/people/business_male_03/opacity.webp +0 -0
  58. package/dist/office/people/business_male_03/portrait.webp +0 -0
  59. package/dist/office/people/business_male_04/avatar.fbx +0 -0
  60. package/dist/office/people/business_male_04/body.webp +0 -0
  61. package/dist/office/people/business_male_04/head.webp +0 -0
  62. package/dist/office/people/business_male_04/portrait.webp +0 -0
  63. package/dist/office/people/business_male_05/avatar.fbx +0 -0
  64. package/dist/office/people/business_male_05/body.webp +0 -0
  65. package/dist/office/people/business_male_05/head.webp +0 -0
  66. package/dist/office/people/business_male_05/portrait.webp +0 -0
  67. package/dist/office/people/business_male_06/avatar.fbx +0 -0
  68. package/dist/office/people/business_male_06/body.webp +0 -0
  69. package/dist/office/people/business_male_06/head.webp +0 -0
  70. package/dist/office/people/business_male_06/portrait.webp +0 -0
  71. package/dist/office/people/business_male_07/avatar.fbx +0 -0
  72. package/dist/office/people/business_male_07/body.webp +0 -0
  73. package/dist/office/people/business_male_07/head.webp +0 -0
  74. package/dist/office/people/business_male_07/portrait.webp +0 -0
  75. package/dist/office/people/female_adult_02/avatar.fbx +0 -0
  76. package/dist/office/people/female_adult_02/body.webp +0 -0
  77. package/dist/office/people/female_adult_02/head.webp +0 -0
  78. package/dist/office/people/female_adult_02/opacity.webp +0 -0
  79. package/dist/office/people/female_adult_02/portrait.webp +0 -0
  80. package/dist/office/people/female_adult_04/avatar.fbx +0 -0
  81. package/dist/office/people/female_adult_04/body.webp +0 -0
  82. package/dist/office/people/female_adult_04/head.webp +0 -0
  83. package/dist/office/people/female_adult_04/opacity.webp +0 -0
  84. package/dist/office/people/female_adult_04/portrait.webp +0 -0
  85. package/dist/office/people/female_adult_05/avatar.fbx +0 -0
  86. package/dist/office/people/female_adult_05/body.webp +0 -0
  87. package/dist/office/people/female_adult_05/head.webp +0 -0
  88. package/dist/office/people/female_adult_05/opacity.webp +0 -0
  89. package/dist/office/people/female_adult_05/portrait.webp +0 -0
  90. package/dist/office/people/female_adult_07/avatar.fbx +0 -0
  91. package/dist/office/people/female_adult_07/body.webp +0 -0
  92. package/dist/office/people/female_adult_07/head.webp +0 -0
  93. package/dist/office/people/female_adult_07/opacity.webp +0 -0
  94. package/dist/office/people/female_adult_07/portrait.webp +0 -0
  95. package/dist/office/people/female_adult_09/avatar.fbx +0 -0
  96. package/dist/office/people/female_adult_09/body.webp +0 -0
  97. package/dist/office/people/female_adult_09/head.webp +0 -0
  98. package/dist/office/people/female_adult_09/opacity.webp +0 -0
  99. package/dist/office/people/female_adult_09/portrait.webp +0 -0
  100. package/dist/office/people/female_adult_10/avatar.fbx +0 -0
  101. package/dist/office/people/female_adult_10/body.webp +0 -0
  102. package/dist/office/people/female_adult_10/head.webp +0 -0
  103. package/dist/office/people/female_adult_10/opacity.webp +0 -0
  104. package/dist/office/people/female_adult_10/portrait.webp +0 -0
  105. package/dist/office/people/female_adult_13/avatar.fbx +0 -0
  106. package/dist/office/people/female_adult_13/body.webp +0 -0
  107. package/dist/office/people/female_adult_13/head.webp +0 -0
  108. package/dist/office/people/female_adult_13/opacity.webp +0 -0
  109. package/dist/office/people/female_adult_13/portrait.webp +0 -0
  110. package/dist/office/people/female_adult_14/avatar.fbx +0 -0
  111. package/dist/office/people/female_adult_14/body.webp +0 -0
  112. package/dist/office/people/female_adult_14/head.webp +0 -0
  113. package/dist/office/people/female_adult_14/opacity.webp +0 -0
  114. package/dist/office/people/female_adult_14/portrait.webp +0 -0
  115. package/dist/office/people/female_adult_15/avatar.fbx +0 -0
  116. package/dist/office/people/female_adult_15/body.webp +0 -0
  117. package/dist/office/people/female_adult_15/head.webp +0 -0
  118. package/dist/office/people/female_adult_15/opacity.webp +0 -0
  119. package/dist/office/people/female_adult_15/portrait.webp +0 -0
  120. package/dist/office/people/female_adult_17/avatar.fbx +0 -0
  121. package/dist/office/people/female_adult_17/body.webp +0 -0
  122. package/dist/office/people/female_adult_17/head.webp +0 -0
  123. package/dist/office/people/female_adult_17/opacity.webp +0 -0
  124. package/dist/office/people/female_adult_17/portrait.webp +0 -0
  125. package/dist/office/people/male_adult_02/avatar.fbx +0 -0
  126. package/dist/office/people/male_adult_02/body.webp +0 -0
  127. package/dist/office/people/male_adult_02/head.webp +0 -0
  128. package/dist/office/people/male_adult_02/opacity.webp +0 -0
  129. package/dist/office/people/male_adult_02/portrait.webp +0 -0
  130. package/dist/office/people/male_adult_03/avatar.fbx +0 -0
  131. package/dist/office/people/male_adult_03/body.webp +0 -0
  132. package/dist/office/people/male_adult_03/head.webp +0 -0
  133. package/dist/office/people/male_adult_03/opacity.webp +0 -0
  134. package/dist/office/people/male_adult_03/portrait.webp +0 -0
  135. package/dist/office/people/male_adult_04/avatar.fbx +0 -0
  136. package/dist/office/people/male_adult_04/body.webp +0 -0
  137. package/dist/office/people/male_adult_04/head.webp +0 -0
  138. package/dist/office/people/male_adult_04/opacity.webp +0 -0
  139. package/dist/office/people/male_adult_04/portrait.webp +0 -0
  140. package/dist/office/people/male_adult_06/avatar.fbx +0 -0
  141. package/dist/office/people/male_adult_06/body.webp +0 -0
  142. package/dist/office/people/male_adult_06/head.webp +0 -0
  143. package/dist/office/people/male_adult_06/opacity.webp +0 -0
  144. package/dist/office/people/male_adult_06/portrait.webp +0 -0
  145. package/dist/office/people/male_adult_08/avatar.fbx +0 -0
  146. package/dist/office/people/male_adult_08/body.webp +0 -0
  147. package/dist/office/people/male_adult_08/head.webp +0 -0
  148. package/dist/office/people/male_adult_08/opacity.webp +0 -0
  149. package/dist/office/people/male_adult_08/portrait.webp +0 -0
  150. package/dist/office/people/male_adult_12/avatar.fbx +0 -0
  151. package/dist/office/people/male_adult_12/body.webp +0 -0
  152. package/dist/office/people/male_adult_12/head.webp +0 -0
  153. package/dist/office/people/male_adult_12/portrait.webp +0 -0
  154. package/dist/office/people/male_adult_13/avatar.fbx +0 -0
  155. package/dist/office/people/male_adult_13/body.webp +0 -0
  156. package/dist/office/people/male_adult_13/head.webp +0 -0
  157. package/dist/office/people/male_adult_13/portrait.webp +0 -0
  158. package/dist/office/people/male_adult_14/avatar.fbx +0 -0
  159. package/dist/office/people/male_adult_14/body.webp +0 -0
  160. package/dist/office/people/male_adult_14/head.webp +0 -0
  161. package/dist/office/people/male_adult_14/portrait.webp +0 -0
  162. package/dist/office/people/male_adult_19/avatar.fbx +0 -0
  163. package/dist/office/people/male_adult_19/body.webp +0 -0
  164. package/dist/office/people/male_adult_19/head.webp +0 -0
  165. package/dist/office/people/male_adult_19/portrait.webp +0 -0
  166. package/dist/office/textures/herringbone_parquet_diff.webp +0 -0
  167. package/dist/office/textures/herringbone_parquet_nor.webp +0 -0
  168. package/dist/office/textures/herringbone_parquet_rough.webp +0 -0
  169. package/dist/office/textures/laminate_floor_02_diff.webp +0 -0
  170. package/dist/office/textures/laminate_floor_02_nor.webp +0 -0
  171. package/dist/office/textures/laminate_floor_02_rough.webp +0 -0
  172. package/dist/office/textures/oak_veneer_detail.webp +0 -0
  173. package/dist/office/textures/oak_veneer_nor.webp +0 -0
  174. package/dist/office/textures/oak_veneer_rough.webp +0 -0
  175. package/docs/ASSETS.md +43 -0
  176. package/docs/licenses/Microsoft-Rocketbox-LICENSE.txt +21 -0
  177. package/docs/marketing/hero.jpg +0 -0
  178. package/package.json +4 -3
  179. package/dist/office/assets/index-77PZXJ_x.js +0 -5529
  180. package/dist/office/assets/index-CB3kPPFD.css +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to piu-piu. Versions follow [semver](https://semver.org).
4
4
 
5
+ ## 0.2.0 (2026-10-09)
6
+
7
+ The office redesign, real people, and a studio to choose how each persona looks.
8
+
9
+ - **A new office:** warm oak, built after a reference render.
10
+ - The open space has lit desk benches with rounded oak waterfall tops, slim chairs, laptops that open and close, and real oak parquet.
11
+ - There are meeting booths with sliding glass doors (olive in the main space, mustard in Studio B) and a boardroom with a boat-shaped oak table.
12
+ - The kitchen, lounge and library have real CC0 plants and furniture from Poly Haven.
13
+ - **Real people:** personas are Microsoft Rocketbox avatars (MIT), driven by piu-piu's own poses. Their hands and feet are checked in every pose.
14
+ - **Persona studio:** a full page to choose how a persona looks, from New persona or a persona's menu (Change look…).
15
+ - Turn the person round on an oak plinth and try them in a few poses.
16
+ - Filter 30 people and outfits by woman / man and business / smart casual, or keep the automatic look.
17
+ - **Office life:**
18
+ - Idle personas play ping pong, baby-foot, darts and the arcade, chat in booths, make coffee, and use the sofas, always in their own suite and never walking through furniture.
19
+ - Waiting personas get an amber desk lamp.
20
+ - Long tasks bring headphones out.
21
+ - Claude agents on call chat on the phone.
22
+ - **Celebrations:** confetti, a pill and a soft bell when a commit or a merge goes through (Settings → Sounds).
23
+ - **Model picker:** the chat's model button, next to the mode, opens Claude Code's own `/model` list. The choice is kept per persona and applies straight away.
24
+ - **Quieter name tags:** in a busy office seen from afar, only the people who need you keep their tag; people at work show a green dot instead. Zoom in or hover to see names.
25
+ - **Dashboard:**
26
+ - room picker;
27
+ - denser, foldable panels;
28
+ - collapsible side panel (`\`);
29
+ - map controls: drag to pan, Ctrl-drag to rotate, zoom to cursor, double-click to focus.
30
+ - **Fix:** removed the ambient light hidden in each avatar file, which over-brightened the office as more people came in.
31
+ - Claude Code's built-in agents (Explore, Plan…) no longer take desks.
32
+ - The license is now MIT.
33
+
34
+ ## 0.1.1 (2026-10-05)
35
+
36
+ - Fix piu-piu setup failing with launchd "Bootstrap failed: 5" when the service was already loaded
37
+
5
38
  ## 0.1.0 (2026-10-05)
6
39
 
7
40
  First release.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 alphousseyni sy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://unpkg.com/piu-piu-cli@0.1.0/docs/banner.svg" alt="piu-piu: a 3D office for your Claude Code agents" width="100%">
2
+ <img src="https://unpkg.com/piu-piu-cli@0.2.0/docs/banner.svg" alt="piu-piu: a 3D office for your Claude Code agents" width="100%">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -13,18 +13,23 @@
13
13
  <img alt="Vite" src="https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=vite&logoColor=white">
14
14
  <img alt="pnpm" src="https://img.shields.io/badge/pnpm-F69220?style=for-the-badge&logo=pnpm&logoColor=white">
15
15
  <img alt="macOS" src="https://img.shields.io/badge/macOS-000000?style=for-the-badge&logo=apple&logoColor=white">
16
+ <img alt="MIT" src="https://img.shields.io/badge/License-MIT-E2B45C?style=for-the-badge">
16
17
  </p>
17
18
 
18
19
  # piu-piu
19
20
 
20
21
  piu-piu shows every Claude Code session on your machine as a person in a 3D isometric office.
21
22
 
22
- - **People:** each session sits at a desk and types while it works. It raises a hand when it needs you, and takes a break in the lounge when it's idle.
23
- - **Specialists:** your Claude Code agents (`~/.claude/agents`, plugins, built-ins) are people with their own desks. They get to work whenever a session calls them.
24
- - **Control:** you talk to any agent from the office. From there you can approve or deny its tool calls, answer its questions, run slash commands and switch modes. You can also resume old conversations, or open the **real Claude Code** in a terminal inside the page.
23
+ - **Personas:** each Claude Code session is a persona: a person at a desk who types while it works. It raises a hand when it needs you, and takes a break in the lounge when it's idle.
24
+ - **Claude agents:** your real Claude Code agents (`~/.claude/agents`, plugins, built-ins) are people with their own desks. They get to work whenever a persona calls them.
25
+ - **Control:** you talk to any persona from the office. From there you can approve or deny its tool calls, answer its questions, run slash commands and switch modes. You can also resume old conversations, or open the **real Claude Code** in a terminal inside the page.
25
26
 
26
27
  It runs locally. One daemon on `127.0.0.1:7777` watches `~/.claude` and serves the office web app. Nothing leaves your machine.
27
28
 
29
+ <p align="center">
30
+ <img src="https://unpkg.com/piu-piu-cli@0.2.0/docs/marketing/hero.jpg" alt="A tour of the piu-piu office: the dashboard, an approval in the chat, the Claude Code terminal, the meeting room, the games, the desks, the booth, the kitchen and Studio B" width="100%">
31
+ </p>
32
+
28
33
  ```bash
29
34
  npm i -g piu-piu
30
35
  piu-piu setup
@@ -40,7 +45,7 @@ piu-piu setup
40
45
  - [Commands](#commands)
41
46
  - [Connect your terminal sessions](#connect-your-terminal-sessions)
42
47
  - [Using the office](#using-the-office)
43
- - [Agents, specialists and skills](#agents-specialists-and-skills)
48
+ - [Personas, Claude agents and skills](#personas-claude-agents-and-skills)
44
49
  - [Configuration](#configuration)
45
50
  - [Running Claude in a VM, office on the host](#running-claude-in-a-vm-office-on-the-host)
46
51
  - [How it works](#how-it-works)
@@ -63,7 +68,7 @@ piu-piu setup
63
68
  | Raised hand and amber light | Waiting for you: a permission prompt or a question |
64
69
  | Head in hands | The session hit an error |
65
70
  | Lounge, ping-pong, baby-foot | Idle for 5+ minutes. Games need at least 2 people on a break; anyone alone gets the sofa and a coffee |
66
- | Dashed coloured line between two people | A specialist (subagent) working for the session it's linked to |
71
+ | Dashed coloured line between two people | A Claude agent (subagent) working for the persona it's linked to |
67
72
  | `using devops-mz` badge | The session is running that skill this turn |
68
73
 
69
74
  ### Talk and control
@@ -125,8 +130,8 @@ pnpm piu-piu setup # same CLI, running from the checkout
125
130
  |---|---|
126
131
  | `piu-piu setup` | Hooks, channel and background service, then opens the office |
127
132
  | `piu-piu open` | Opens the office in your browser |
128
- | `piu-piu status` | Sessions open, working and waiting; specialists at work; approvals; terminals |
129
- | `piu-piu start` · `stop` · `restart` | The background service. `stop` and `restart` refuse while an agent is working in the office; add `--force` to go ahead anyway |
133
+ | `piu-piu status` | Sessions open, working and waiting; Claude agents at work; approvals; terminals |
134
+ | `piu-piu start` · `stop` · `restart` | The background service. `stop` and `restart` refuse while a persona is working in the office; add `--force` to go ahead anyway |
130
135
  | `piu-piu run` | Runs in the foreground instead of as a service |
131
136
  | `piu-piu logs [-f]` | Shows or follows the log |
132
137
  | `piu-piu hooks install` · `uninstall` | Claude Code hooks only |
@@ -170,15 +175,15 @@ Messages you send from the office arrive in that terminal session, and Claude an
170
175
  |---|---|
171
176
  | Open someone's conversation | Click their figure, or pick them in **Team** |
172
177
  | Jump to any agent | `⌘K` / `Ctrl+K` |
173
- | Rotate / pan / zoom | Drag / right-drag / scroll |
178
+ | Move / rotate / zoom | Drag / Ctrl-click drag / scroll (or + −) |
174
179
  | Dock the sidebar beside the office | Dock button in the sidebar header |
175
180
  | Close the switcher, collapse full screen, close the sidebar | `Esc` (in that order) |
176
181
  | Lighting | Auto (follows your clock), day, sunset, night |
177
182
 
178
183
  ### Sidebar
179
- - **Team:** everyone, grouped by team, with filters for Working, Waiting and On a break. Specialists are listed at the bottom.
184
+ - **Team:** everyone, grouped by team, with filters for Working, Waiting and On a break. Claude agents are listed at the bottom.
180
185
  - **Inbox:** every approval and question waiting for you, across all agents.
181
- - **Activity:** a live feed of prompts, tools, turns, and specialists being called.
186
+ - **Activity:** a live feed of prompts, tools, turns, and Claude agents being called.
182
187
  - **Conversation:**
183
188
  - The project bar shows the folder, git branch and path.
184
189
  - The status strip shows the context window and session cost.
@@ -199,16 +204,18 @@ Messages you send from the office arrive in that terminal session, and Claude an
199
204
 
200
205
  ---
201
206
 
202
- ## Agents, specialists and skills
207
+ ## Personas, Claude agents and skills
208
+
209
+ piu-piu has two kinds of people: **personas** (your Claude Code chat sessions) and **Claude agents** (real Claude Code agent definitions, called by personas as subagents). In the code and API, Claude agents are still called *specialists*.
203
210
 
204
211
  | Kind | What it is | How it shows |
205
212
  |---|---|---|
206
- | **Agent** | A folder you work in: from `agents.yaml`, created in the office, or a "freelancer" for any other folder with a session | A person at a desk. One figure per agent; a second figure only for a parallel busy terminal |
207
- | **Specialist** | A Claude Code agent definition: `~/.claude/agents/*.md`, `<project>/.claude/agents/*.md`, enabled plugin agents, and built-ins (Explore, Plan, general-purpose, claude-code-guide, statusline-setup, fork) | A person with a home desk, wearing the agent's `color`. Gets a desk once called in the last 30 days; the rest are listed "on call" |
213
+ | **Persona** | A Claude Code session in a folder you work in: from `agents.yaml`, created in the office, or a "freelancer" for any other folder with a session | A person at a desk. One figure per persona; a second figure only for a parallel busy terminal |
214
+ | **Claude agent** | A Claude Code agent definition: `~/.claude/agents/*.md`, `<project>/.claude/agents/*.md`, enabled plugin agents, and built-ins (Explore, Plan, general-purpose, claude-code-guide, statusline-setup, fork) | A person with a home desk, wearing the agent's `color`. Gets a desk once called in the last 30 days; the rest are listed "on call" |
208
215
  | **Skill** | `~/.claude/skills/<name>` or a project skill. It runs **inside** the calling session | A `using <skill>` badge on that session |
209
216
 
210
- **Specialists in practice**
211
- - **When a session calls one** (`Task` / `Agent` tool), the specialist works at their own desk. A dashed line in their colour runs to the caller, and their label reads `For Lamine · Read: src/app.ts`.
217
+ **Claude agents in practice**
218
+ - **When a persona calls one** (`Task` / `Agent` tool), the Claude agent works at their own desk. A dashed line in their colour runs to the caller, and their label reads `For Lamine · Read: src/app.ts`.
212
219
  - **Parallel calls** of the same agent bring in colleagues ("Explore · 2") at free desks.
213
220
  - **Each call opens read-only in the sidebar:** the caller's brief, the tools it used and the answer it handed back. A caller's chat shows a **Helping now** strip, and its `Agent:` tool rows link to the call.
214
221
  - **Home rooms** come from the agent's name and description:
@@ -235,7 +242,7 @@ agents:
235
242
  Prefer small PRs.
236
243
  permissionMode: default # optional starting mode: default | acceptEdits | plan
237
244
 
238
- # optional: seat specialists in a specific room
245
+ # optional: seat Claude agents in a specific room
239
246
  specialists:
240
247
  mazestudio-docs: library
241
248
  devops-agent: server-room
@@ -311,8 +318,8 @@ Local only. Browser requests must come from the office's own origin (see [Securi
311
318
  |---|---|
312
319
  | `GET /api/state` | Snapshot: roster, sessions, events, approvals, running agents, terminals, specialists, recent subagent runs |
313
320
  | `GET /api/sessions/:id/transcript?before=<offset>` | A session's own conversation, paged by byte offset |
314
- | `GET /api/subagents/:id/transcript` | A specialist call's own conversation |
315
- | `GET /api/specialists/:type/runs` | Recent calls of one specialist |
321
+ | `GET /api/subagents/:id/transcript` | A Claude agent call's own conversation |
322
+ | `GET /api/specialists/:type/runs` | Recent calls of one Claude agent |
316
323
  | `GET /api/agents/:id/chat` | Office chat history |
317
324
  | `POST /api/agents/:id/messages` `{text}` | Send a message (or `/command`) to an agent |
318
325
  | `POST /api/agents/:id/stop` · `/reset` | Stop the current task · start a new conversation |
@@ -353,7 +360,7 @@ Local only. Browser requests must come from the office's own origin (see [Securi
353
360
  | Can't type to an open terminal session | Start it with `--dangerously-load-development-channels server:piu-piu`, or close it and continue from the office |
354
361
  | Terminal shows "Do you trust this folder?" | That's Claude Code's first-run prompt for that folder. Answer it once |
355
362
  | `posix_spawnp failed` when opening a terminal | Reinstall (`npm i -g piu-piu-cli`): the install step fixes node-pty's spawn-helper permissions |
356
- | A specialist stays "working" | Runs end on handback, on SubagentStop, or after 10 minutes without activity. Install hooks for instant updates |
363
+ | A Claude agent stays "working" | Runs end on handback, on SubagentStop, or after 10 minutes without activity. Install hooks for instant updates |
357
364
  | Logs | `piu-piu logs -f` |
358
365
 
359
366
  ---
@@ -404,3 +411,9 @@ piu-piu uninstall # service, hooks and channel
404
411
  npm rm -g piu-piu
405
412
  rm -rf ~/.piu-piu # your data, if you want it gone too
406
413
  ```
414
+
415
+ ---
416
+
417
+ ## License
418
+
419
+ [MIT](LICENSE)
package/bin/piu-piu.mjs CHANGED
@@ -33,6 +33,22 @@ const fail = (msg) => {
33
33
  process.exit(1);
34
34
  };
35
35
  const quiet = (cmd, args) => spawnSync(cmd, args, { stdio: 'pipe', encoding: 'utf8' });
36
+ const sleepSync = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
37
+
38
+ // launchd unloads asynchronously: loading again too soon fails with
39
+ // "Bootstrap failed: 5". Unload, wait until it's gone, then load (retrying once).
40
+ function launchdUnload(target) {
41
+ quiet('launchctl', ['bootout', target]);
42
+ for (let i = 0; i < 40 && quiet('launchctl', ['print', target]).status === 0; i++) sleepSync(250);
43
+ }
44
+ function launchdLoad(domain, plist) {
45
+ let r = quiet('launchctl', ['bootstrap', domain, plist]);
46
+ for (let i = 0; i < 3 && r.status !== 0; i++) {
47
+ sleepSync(1000);
48
+ r = quiet('launchctl', ['bootstrap', domain, plist]);
49
+ }
50
+ return r;
51
+ }
36
52
 
37
53
  function claudeBinary() {
38
54
  for (const p of [join(homedir(), '.local', 'bin', 'claude'), '/opt/homebrew/bin/claude', '/usr/local/bin/claude']) if (existsSync(p)) return p;
@@ -138,9 +154,9 @@ function serviceInstall() {
138
154
  if (MAC) {
139
155
  const domain = `gui/${process.getuid()}`;
140
156
  // the pre-rename service, if this Mac still has it
141
- quiet('launchctl', ['bootout', `${domain}/${LEGACY_LABEL}`]);
157
+ launchdUnload(`${domain}/${LEGACY_LABEL}`);
142
158
  rmSync(join(homedir(), 'Library', 'LaunchAgents', `${LEGACY_LABEL}.plist`), { force: true });
143
- quiet('launchctl', ['bootout', `${domain}/${LABEL}`]);
159
+ launchdUnload(`${domain}/${LABEL}`);
144
160
  const env = [['PATH', servicePath()], ['HOME', homedir()], ...passEnv()].map(([k, v]) => ` <key>${esc(k)}</key><string>${esc(v)}</string>`).join('\n');
145
161
  mkdirSync(dirname(PLIST), { recursive: true });
146
162
  mkdirSync(dirname(LOG), { recursive: true });
@@ -170,8 +186,8 @@ ${env}
170
186
  </plist>
171
187
  `,
172
188
  );
173
- const r = quiet('launchctl', ['bootstrap', domain, PLIST]);
174
- if (r.status !== 0) fail(`launchctl could not start the service: ${r.stderr}`);
189
+ const r = launchdLoad(domain, PLIST);
190
+ if (r.status !== 0) fail(`launchctl could not start the service: ${r.stderr.trim()}. Try: piu-piu start`);
175
191
  } else {
176
192
  if (quiet('systemctl', ['--user', '--version']).status !== 0) fail('No systemd user session here. Run the office in the foreground instead: piu-piu run');
177
193
  mkdirSync(dirname(UNIT), { recursive: true });
@@ -204,7 +220,7 @@ WantedBy=default.target
204
220
 
205
221
  function serviceUninstall() {
206
222
  if (MAC) {
207
- quiet('launchctl', ['bootout', `gui/${process.getuid()}/${LABEL}`]);
223
+ launchdUnload(`gui/${process.getuid()}/${LABEL}`);
208
224
  rmSync(PLIST, { force: true });
209
225
  } else {
210
226
  quiet('systemctl', ['--user', 'disable', '--now', 'piu-piu.service']);
@@ -231,7 +247,7 @@ function open() {
231
247
  async function start() {
232
248
  if (await state()) return say(c.ok('✓'), `piu-piu is running at ${c.b(URL)}`);
233
249
  if (!serviceInstalled()) serviceInstall();
234
- else if (MAC) quiet('launchctl', ['bootstrap', `gui/${process.getuid()}`, PLIST]);
250
+ else if (MAC) launchdLoad(`gui/${process.getuid()}`, PLIST);
235
251
  else quiet('systemctl', ['--user', 'start', 'piu-piu.service']);
236
252
  if (!(await waitUp())) fail(`piu-piu didn't come up. Check the logs: piu-piu logs`);
237
253
  say(c.ok('✓'), `piu-piu is running at ${c.b(URL)}`);
@@ -239,7 +255,7 @@ async function start() {
239
255
 
240
256
  async function stop() {
241
257
  await guardBusy('Stopping');
242
- if (MAC) quiet('launchctl', ['bootout', `gui/${process.getuid()}/${LABEL}`]);
258
+ if (MAC) launchdUnload(`gui/${process.getuid()}/${LABEL}`);
243
259
  else quiet('systemctl', ['--user', 'stop', 'piu-piu.service']);
244
260
  say(c.ok('✓'), 'piu-piu stopped. Start it again with: piu-piu start');
245
261
  }
package/dist/daemon.mjs CHANGED
@@ -25,7 +25,8 @@ var config = {
25
25
  // "Working" with no activity for this long means the session stalled or was
26
26
  // killed without telling us; show it as idle.
27
27
  stalledMs: 10 * 60 * 1e3,
28
- maxEvents: 2e3
28
+ maxEvents: 1e5
29
+ // a week of busy days; older events are pruned too
29
30
  };
30
31
  var ROSTER_TEMPLATE = `# piu-piu roster. Sessions whose folder is inside an agent's cwd sit at that agent's desk;
31
32
  # anything else gets a "freelancer" desk named after its folder.
@@ -89,12 +90,15 @@ function loadAppAgents() {
89
90
  function saveAppAgents(agents) {
90
91
  writeFileSync2(appAgentsPath(), JSON.stringify(agents.map(({ managed: _m, ...a }) => a), null, 2) + "\n");
91
92
  }
92
- function renameYamlAgent(id, name, path = config.rosterPath) {
93
+ function updateYamlAgent(id, fields, path = config.rosterPath) {
93
94
  const doc = parseDocument(readFileSync2(path, "utf8"));
94
95
  const list = doc.get("agents");
95
96
  const item = list?.items?.find((it) => it.get("id") === id);
96
97
  if (!item) return false;
97
- item.set("name", name);
98
+ for (const [k, v] of Object.entries(fields)) {
99
+ if (v === void 0) item.delete(k);
100
+ else item.set(k, v);
101
+ }
98
102
  writeFileSync2(path, doc.toString());
99
103
  return true;
100
104
  }
@@ -740,7 +744,7 @@ var AgentRuntime = class extends EventEmitter4 {
740
744
  resume = closed?.sessionId ?? this.store.agentSession(agentId);
741
745
  if (resume) this.store.setAgentSession(agentId, resume);
742
746
  }
743
- this.terminals.start(agentId, { cwd, resume, mode: this.mode(agentId) }, cols, rows);
747
+ this.terminals.start(agentId, { cwd, resume, mode: this.mode(agentId), model: this.model(agentId) }, cols, rows);
744
748
  this.say(
745
749
  agentId,
746
750
  "status",
@@ -853,6 +857,27 @@ var AgentRuntime = class extends EventEmitter4 {
853
857
  }
854
858
  await l.query.setPermissionMode(mode);
855
859
  }
860
+ // The model picked in the office, if any (otherwise Claude Code's default).
861
+ model(agentId) {
862
+ return this.store.agentModel(agentId);
863
+ }
864
+ // Like /model in the terminal; applies to the running conversation too.
865
+ async setModel(agentId, model) {
866
+ this.agent(agentId);
867
+ const value = model && model !== "default" ? model : void 0;
868
+ if (value === this.model(agentId)) return;
869
+ this.store.setAgentModel(agentId, value);
870
+ const l = this.live.get(agentId);
871
+ if (!l) return;
872
+ await l.query.setModel(value);
873
+ const name = this.commandCache.get(agentId)?.models.find((m) => m.value === (model ?? "default"))?.displayName ?? model ?? "the default model";
874
+ this.say(agentId, "status", `Switched to ${name}.`);
875
+ }
876
+ // The models this agent's sessions can use, as Claude Code's /model lists them.
877
+ async models(agentId) {
878
+ await this.commands(agentId);
879
+ return this.commandCache.get(agentId)?.models ?? [];
880
+ }
856
881
  // The slash commands this agent's sessions support (built-ins, skills,
857
882
  // custom commands), minus the ones that only make sense in a terminal.
858
883
  // Loading them starts Claude Code without sending any message.
@@ -869,8 +894,9 @@ var AgentRuntime = class extends EventEmitter4 {
869
894
  options: { cwd: this.cwdOverride.get(agentId) ?? a.cwd, settingSources: ["user", "project", "local"] }
870
895
  });
871
896
  try {
897
+ const models = (await q.supportedModels()).map((m) => ({ value: m.value, displayName: m.displayName, description: m.description, resolvedModel: m.resolvedModel }));
872
898
  const list = (await q.supportedCommands()).filter((c) => !this.terminalOnly.has(c.name) && !c.name.startsWith("_")).map((c) => ({ name: c.name, description: c.description, argumentHint: c.argumentHint || void 0 })).sort((x, y) => x.name.localeCompare(y.name));
873
- this.commandCache.set(agentId, { at: Date.now(), list });
899
+ this.commandCache.set(agentId, { at: Date.now(), list, models });
874
900
  return list;
875
901
  } finally {
876
902
  release();
@@ -1023,6 +1049,7 @@ var AgentRuntime = class extends EventEmitter4 {
1023
1049
  systemPrompt: { type: "preset", preset: "claude_code", append: instructions },
1024
1050
  settingSources: ["user", "project", "local"],
1025
1051
  permissionMode: this.mode(agentId),
1052
+ model: this.model(agentId),
1026
1053
  // only processes started in "Allow all" may bypass permissions in the CLI;
1027
1054
  // switching to it later is honoured by ask()
1028
1055
  allowDangerouslySkipPermissions: this.mode(agentId) === "bypassPermissions",
@@ -1169,6 +1196,7 @@ var Terminals = class extends EventEmitter5 {
1169
1196
  else if (opts.resume) args.push("--resume", opts.resume);
1170
1197
  if (opts.mode === "bypassPermissions") args.push("--dangerously-skip-permissions");
1171
1198
  else if (opts.mode === "plan" || opts.mode === "acceptEdits") args.push("--permission-mode", opts.mode);
1199
+ if (opts.model) args.push("--model", opts.model);
1172
1200
  ensureSpawnHelper();
1173
1201
  const proc = pty.spawn(claudeBinary(), args, {
1174
1202
  name: "xterm-256color",
@@ -1228,6 +1256,7 @@ var Terminals = class extends EventEmitter5 {
1228
1256
 
1229
1257
  // ../../apps/daemon/src/server.ts
1230
1258
  import { existsSync as existsSync4 } from "node:fs";
1259
+ import { userInfo } from "node:os";
1231
1260
  import { join as join7 } from "node:path";
1232
1261
  import fastifyStatic from "@fastify/static";
1233
1262
  import fastifyWebsocket from "@fastify/websocket";
@@ -1534,6 +1563,17 @@ async function startServer(office2, runtime2, store2, terminals2, specialists2,
1534
1563
  subagents: subagents2.recent()
1535
1564
  });
1536
1565
  app2.get("/api/state", async () => snapshot());
1566
+ app2.get("/api/stats", async (req) => {
1567
+ const hours = Math.min(48, Math.max(1, Number(req.query.hours) || 12));
1568
+ const midnight = /* @__PURE__ */ new Date();
1569
+ midnight.setHours(0, 0, 0, 0);
1570
+ const name = userInfo().username;
1571
+ return {
1572
+ user: name.charAt(0).toUpperCase() + name.slice(1),
1573
+ spentToday: store2.spentSince(midnight.getTime()),
1574
+ activity: store2.activity(hours)
1575
+ };
1576
+ });
1537
1577
  app2.get("/api/sessions/:id/transcript", async (req, reply) => {
1538
1578
  const path = office2.transcriptPaths.get(req.params.id);
1539
1579
  if (!path) return reply.code(404).send({ error: "No transcript for that session yet." });
@@ -1583,7 +1623,9 @@ async function startServer(office2, runtime2, store2, terminals2, specialists2,
1583
1623
  const base = name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent";
1584
1624
  let id = base;
1585
1625
  for (let i = 2; office2.roster.some((a) => a.id === id); i++) id = `${base}-${i}`;
1586
- const agent = { id, name, role: req.body?.role?.trim() || "dev", room, cwd: expandHome(cwd) };
1626
+ const avatar = req.body?.avatar?.trim();
1627
+ if (avatar && !/^[a-z0-9_]{1,40}$/.test(avatar)) return reply.code(400).send({ error: "Unknown look." });
1628
+ const agent = { id, name, role: req.body?.role?.trim() || "dev", room, cwd: expandHome(cwd), ...avatar ? { avatar } : {} };
1587
1629
  runtime2.forget(agent.id);
1588
1630
  saveAppAgents([...appAgents(), agent]);
1589
1631
  if (req.body?.adopt?.startsWith("freelancer:")) store2.moveAgent(req.body.adopt, agent.id);
@@ -1591,13 +1633,23 @@ async function startServer(office2, runtime2, store2, terminals2, specialists2,
1591
1633
  return reply.code(201).send(agent);
1592
1634
  });
1593
1635
  app2.patch("/api/agents/:id", async (req, reply) => {
1594
- const name = req.body?.name?.trim();
1595
- if (!name) return reply.code(400).send({ error: "The name can\u2019t be empty." });
1636
+ const fields = {};
1637
+ if (req.body?.name !== void 0) {
1638
+ const name = req.body.name.trim();
1639
+ if (!name) return reply.code(400).send({ error: "The name can\u2019t be empty." });
1640
+ fields.name = name;
1641
+ }
1642
+ if (req.body?.avatar !== void 0) {
1643
+ const avatar = req.body.avatar.trim();
1644
+ if (avatar && !/^[a-z0-9_]{1,40}$/.test(avatar)) return reply.code(400).send({ error: "Unknown look." });
1645
+ fields.avatar = avatar || void 0;
1646
+ }
1647
+ if (!("name" in fields) && !("avatar" in fields)) return reply.code(400).send({ error: "Nothing to change." });
1596
1648
  const mine = appAgents();
1597
- if (mine.some((a) => a.id === req.params.id)) saveAppAgents(mine.map((a) => a.id === req.params.id ? { ...a, name } : a));
1598
- else if (!renameYamlAgent(req.params.id, name)) return reply.code(404).send({ error: "Unknown agent." });
1649
+ if (mine.some((a) => a.id === req.params.id)) saveAppAgents(mine.map((a) => a.id === req.params.id ? { ...a, ...fields } : a));
1650
+ else if (!updateYamlAgent(req.params.id, fields)) return reply.code(404).send({ error: "Unknown agent." });
1599
1651
  reloadRoster();
1600
- return { ok: true, name };
1652
+ return { ok: true, ...fields };
1601
1653
  });
1602
1654
  app2.delete("/api/agents/:id", async (req, reply) => {
1603
1655
  const mine = appAgents();
@@ -1629,10 +1681,21 @@ async function startServer(office2, runtime2, store2, terminals2, specialists2,
1629
1681
  });
1630
1682
  app2.get("/api/agents/:id/commands", async (req, reply) => {
1631
1683
  try {
1632
- return { commands: await runtime2.commands(req.params.id), mode: runtime2.mode(req.params.id) };
1684
+ const id = req.params.id;
1685
+ return { commands: await runtime2.commands(id), mode: runtime2.mode(id), models: await runtime2.models(id), model: runtime2.model(id) ?? "default" };
1686
+ } catch (err) {
1687
+ return reply.code(404).send({ error: err.message });
1688
+ }
1689
+ });
1690
+ app2.post("/api/agents/:id/model", async (req, reply) => {
1691
+ const model = req.body?.model;
1692
+ if (typeof model !== "string" || !/^[\w.\[\]-]{1,80}$/.test(model)) return reply.code(400).send({ error: "model must be a model name or alias" });
1693
+ try {
1694
+ await runtime2.setModel(req.params.id, model);
1633
1695
  } catch (err) {
1634
1696
  return reply.code(404).send({ error: err.message });
1635
1697
  }
1698
+ return { model };
1636
1699
  });
1637
1700
  app2.post("/api/agents/:id/mode", async (req, reply) => {
1638
1701
  const mode = req.body?.mode;
@@ -1719,10 +1782,12 @@ var Store = class {
1719
1782
  PRAGMA journal_mode = WAL;
1720
1783
  CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, last_activity INTEGER, json TEXT NOT NULL);
1721
1784
  CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER, session_id TEXT, json TEXT NOT NULL);
1785
+ CREATE INDEX IF NOT EXISTS events_ts ON events (ts);
1722
1786
  CREATE TABLE IF NOT EXISTS chat (id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL, ts INTEGER NOT NULL, role TEXT NOT NULL, text TEXT NOT NULL);
1723
1787
  CREATE INDEX IF NOT EXISTS chat_agent ON chat (agent_id, id);
1724
1788
  CREATE TABLE IF NOT EXISTS agent_sessions (agent_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, updated INTEGER NOT NULL);
1725
1789
  CREATE TABLE IF NOT EXISTS agent_modes (agent_id TEXT PRIMARY KEY, mode TEXT NOT NULL);
1790
+ CREATE TABLE IF NOT EXISTS agent_models (agent_id TEXT PRIMARY KEY, model TEXT NOT NULL);
1726
1791
  `);
1727
1792
  }
1728
1793
  saveSession(s) {
@@ -1735,6 +1800,23 @@ var Store = class {
1735
1800
  addEvent(e) {
1736
1801
  this.db.prepare("INSERT INTO events (ts, session_id, json) VALUES (?, ?, ?)").run(e.ts, e.sessionId, JSON.stringify(e));
1737
1802
  }
1803
+ // Tool calls per hour for the last `hours` hours, oldest first (for the overview chart).
1804
+ activity(hours) {
1805
+ const hourMs = 60 * 60 * 1e3;
1806
+ const end = Math.floor(Date.now() / hourMs) * hourMs + hourMs;
1807
+ const start = end - hours * hourMs;
1808
+ const rows = this.db.prepare("SELECT CAST(ts / 3600000 AS INTEGER) * 3600000 AS hour, COUNT(*) AS n FROM events WHERE ts >= ? AND json_extract(json, '$.kind') = 'tool_start' GROUP BY hour").all(start);
1809
+ const byHour = new Map(rows.map((r) => [Number(r.hour), Number(r.n)]));
1810
+ return Array.from({ length: hours }, (_, i) => {
1811
+ const hour = start + i * hourMs;
1812
+ return { hour, tools: byHour.get(hour) ?? 0 };
1813
+ });
1814
+ }
1815
+ // What the sessions active since `since` cost, including ones already closed.
1816
+ spentSince(since) {
1817
+ const rows = this.db.prepare("SELECT json FROM sessions WHERE last_activity >= ?").all(since);
1818
+ return rows.reduce((sum, r) => sum + (JSON.parse(r.json).costUSD ?? 0), 0);
1819
+ }
1738
1820
  recentEvents(limit) {
1739
1821
  const rows = this.db.prepare("SELECT json FROM events ORDER BY id DESC LIMIT ?").all(limit);
1740
1822
  return rows.map((r) => JSON.parse(r.json)).reverse();
@@ -1754,11 +1836,20 @@ var Store = class {
1754
1836
  setAgentMode(agentId, mode) {
1755
1837
  this.db.prepare("INSERT INTO agent_modes (agent_id, mode) VALUES (?, ?) ON CONFLICT(agent_id) DO UPDATE SET mode = excluded.mode").run(agentId, mode);
1756
1838
  }
1839
+ // The model picked for an agent in the office (none: Claude Code's default).
1840
+ agentModel(agentId) {
1841
+ return this.db.prepare("SELECT model FROM agent_models WHERE agent_id = ?").get(agentId)?.model;
1842
+ }
1843
+ setAgentModel(agentId, model) {
1844
+ if (!model) this.db.prepare("DELETE FROM agent_models WHERE agent_id = ?").run(agentId);
1845
+ else this.db.prepare("INSERT INTO agent_models (agent_id, model) VALUES (?, ?) ON CONFLICT(agent_id) DO UPDATE SET model = excluded.model").run(agentId, model);
1846
+ }
1757
1847
  // An agent was removed (or an id is being reused): drop its saved mode and
1758
1848
  // conversation link, so a later agent with the same id starts clean rather
1759
1849
  // than inheriting, say, Allow all.
1760
1850
  forgetAgent(agentId) {
1761
1851
  this.db.prepare("DELETE FROM agent_modes WHERE agent_id = ?").run(agentId);
1852
+ this.db.prepare("DELETE FROM agent_models WHERE agent_id = ?").run(agentId);
1762
1853
  this.db.prepare("DELETE FROM agent_sessions WHERE agent_id = ?").run(agentId);
1763
1854
  }
1764
1855
  // A freelancer became a named agent: their office chat and conversation follow.
@@ -1766,6 +1857,7 @@ var Store = class {
1766
1857
  this.db.prepare("UPDATE chat SET agent_id = ? WHERE agent_id = ?").run(to, from);
1767
1858
  this.db.prepare("UPDATE OR REPLACE agent_sessions SET agent_id = ? WHERE agent_id = ?").run(to, from);
1768
1859
  this.db.prepare("UPDATE OR REPLACE agent_modes SET agent_id = ? WHERE agent_id = ?").run(to, from);
1860
+ this.db.prepare("UPDATE OR REPLACE agent_models SET agent_id = ? WHERE agent_id = ?").run(to, from);
1769
1861
  }
1770
1862
  // The SDK session each roster agent continues when you message them.
1771
1863
  agentSession(agentId) {
@@ -1778,6 +1870,7 @@ var Store = class {
1778
1870
  }
1779
1871
  prune() {
1780
1872
  this.db.prepare("DELETE FROM events WHERE id <= (SELECT MAX(id) FROM events) - ?").run(config.maxEvents);
1873
+ this.db.prepare("DELETE FROM events WHERE ts < ?").run(Date.now() - 7 * 24 * 60 * 60 * 1e3);
1781
1874
  this.db.prepare("DELETE FROM sessions WHERE last_activity < ?").run(Date.now() - 7 * 24 * 60 * 60 * 1e3);
1782
1875
  }
1783
1876
  };
@@ -1786,6 +1879,40 @@ var Store = class {
1786
1879
  import { open as open4, stat as stat4 } from "node:fs/promises";
1787
1880
  import { basename as basename5 } from "node:path";
1788
1881
  import chokidar3 from "chokidar";
1882
+
1883
+ // ../../apps/daemon/src/observe/gitEvents.ts
1884
+ function gitIntent(command) {
1885
+ if (typeof command !== "string") return void 0;
1886
+ const cmd = command.replace(/<<-?\s*['"]?(\w+)['"]?[\s\S]*?\n\s*\1\b/g, " ").replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, " ").replace(/\s+/g, " ");
1887
+ if (/\bgh pr merge\b/.test(cmd) || /\bgit (?:-C \S+ )?merge\b(?! --abort)/.test(cmd)) return "merge";
1888
+ if (/\bgit (?:-C \S+ )?commit\b/.test(cmd) && !/--dry-run/.test(cmd)) return "commit";
1889
+ return void 0;
1890
+ }
1891
+ function gitOutcome(intent, output, isError, command = "") {
1892
+ if (isError) return void 0;
1893
+ if (intent === "commit") {
1894
+ const m = /^\[[^\]\n]*?\b[0-9a-f]{7,40}\] (.+)$/m.exec(output);
1895
+ if (m) return { kind: "commit", detail: m[1].trim().slice(0, 140) };
1896
+ if (output.trim() === "" && /\bcommit\b[^|;&]*(?:-q\b|--quiet\b|-\w*q\w*m\b)/.test(command)) {
1897
+ const msg = /-\w*m\s+(?:"((?:[^"\\]|\\.)*)"|'([^']*)')/.exec(command);
1898
+ const first = (msg?.[1] ?? msg?.[2] ?? "").split("\n")[0].trim();
1899
+ return { kind: "commit", detail: first ? first.slice(0, 140) : void 0 };
1900
+ }
1901
+ return void 0;
1902
+ }
1903
+ if (/\b(?:fail|error|conflict|not mergeable|aborting)\b/i.test(output) && !/\bMerged\b/.test(output)) return void 0;
1904
+ const pr = /Merged pull request #?(\d+)|Merged #?(\d+)/i.exec(output);
1905
+ if (pr) return { kind: "merge", detail: `Pull request #${pr[1] ?? pr[2]} merged` };
1906
+ if (/Fast-forward|Merge made by/.test(output)) return { kind: "merge", detail: "Branch merged" };
1907
+ return /\bgh pr merge\b/.test(output) || output.trim() === "" ? { kind: "merge", detail: "Pull request merged" } : void 0;
1908
+ }
1909
+ function resultText(content) {
1910
+ if (typeof content === "string") return content;
1911
+ if (Array.isArray(content)) return content.map((c) => typeof c?.text === "string" ? c.text : "").join("\n");
1912
+ return "";
1913
+ }
1914
+
1915
+ // ../../apps/daemon/src/observe/transcripts.ts
1789
1916
  function watchTranscripts(office2) {
1790
1917
  const cursors = /* @__PURE__ */ new Map();
1791
1918
  const queue = /* @__PURE__ */ new Map();
@@ -1798,7 +1925,7 @@ function watchTranscripts(office2) {
1798
1925
  const { size, mtimeMs } = await stat4(path);
1799
1926
  let cur = cursors.get(path);
1800
1927
  if (!cur) {
1801
- cur = { offset: 0, partial: "", pending: {}, replaying: true };
1928
+ cur = { offset: 0, partial: "", pending: {}, replaying: true, gitCalls: /* @__PURE__ */ new Map() };
1802
1929
  cursors.set(path, cur);
1803
1930
  if (Date.now() - mtimeMs > config.replayWindowMs) {
1804
1931
  cur.offset = size;
@@ -1904,6 +2031,8 @@ function watchTranscripts(office2) {
1904
2031
  patch.currentTool = block.name;
1905
2032
  patch.currentToolDetail = detail;
1906
2033
  if (block.name === "Skill" && skillOf(block.input)) patch.activeSkill = skillOf(block.input);
2034
+ const intent = block.name === "Bash" ? gitIntent(block.input?.command) : void 0;
2035
+ if (intent) cur.gitCalls.set(block.id, { kind: intent, command: String(block.input?.command ?? "") });
1907
2036
  emit("tool_start", { tool: block.name, detail }, block.id);
1908
2037
  } else if (block.type === "text" && block.text?.trim()) {
1909
2038
  const text = block.text.trim();
@@ -1926,6 +2055,12 @@ function watchTranscripts(office2) {
1926
2055
  for (const b of content) {
1927
2056
  if (b.type !== "tool_result") continue;
1928
2057
  emit("tool_end", { detail: b.is_error ? "error" : void 0 }, b.tool_use_id);
2058
+ const intent = cur.gitCalls.get(b.tool_use_id);
2059
+ if (intent) {
2060
+ cur.gitCalls.delete(b.tool_use_id);
2061
+ const done = gitOutcome(intent.kind, resultText(b.content), Boolean(b.is_error), intent.command);
2062
+ if (done) emit(done.kind, { detail: done.detail }, b.tool_use_id);
2063
+ }
1929
2064
  }
1930
2065
  apply({ lastActivity: ts, status: "working", currentTool: void 0, currentToolDetail: void 0 });
1931
2066
  return;