piu-piu-cli 0.1.1 → 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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +33 -20
- package/dist/daemon.mjs +148 -13
- package/dist/office/assets/index-CPvN_60E.css +1 -0
- package/dist/office/assets/index-DUMIjmLF.js +5971 -0
- package/dist/office/index.html +3 -3
- package/dist/office/models/anthurium_botany_01.glb +0 -0
- package/dist/office/models/calathea_orbifolia_01.glb +0 -0
- package/dist/office/models/ceramic_vase_01.glb +0 -0
- package/dist/office/models/ceramic_vase_02.glb +0 -0
- package/dist/office/models/ceramic_vase_03.glb +0 -0
- package/dist/office/models/ceramic_vase_04.glb +0 -0
- package/dist/office/models/fern_02.glb +0 -0
- package/dist/office/models/mid_century_lounge_chair.glb +0 -0
- package/dist/office/models/modern_arm_chair_01.glb +0 -0
- package/dist/office/models/pachira_aquatica_01.glb +0 -0
- package/dist/office/models/potted_plant_01.glb +0 -0
- package/dist/office/models/potted_plant_02.glb +0 -0
- package/dist/office/models/potted_plant_04.glb +0 -0
- package/dist/office/models/standing_picture_frame_01.glb +0 -0
- package/dist/office/models/standing_picture_frame_02.glb +0 -0
- package/dist/office/people/business_female_01/avatar.fbx +0 -0
- package/dist/office/people/business_female_01/body.webp +0 -0
- package/dist/office/people/business_female_01/head.webp +0 -0
- package/dist/office/people/business_female_01/opacity.webp +0 -0
- package/dist/office/people/business_female_01/portrait.webp +0 -0
- package/dist/office/people/business_female_02/avatar.fbx +0 -0
- package/dist/office/people/business_female_02/body.webp +0 -0
- package/dist/office/people/business_female_02/head.webp +0 -0
- package/dist/office/people/business_female_02/opacity.webp +0 -0
- package/dist/office/people/business_female_02/portrait.webp +0 -0
- package/dist/office/people/business_female_03/avatar.fbx +0 -0
- package/dist/office/people/business_female_03/body.webp +0 -0
- package/dist/office/people/business_female_03/head.webp +0 -0
- package/dist/office/people/business_female_03/opacity.webp +0 -0
- package/dist/office/people/business_female_03/portrait.webp +0 -0
- package/dist/office/people/business_female_04/avatar.fbx +0 -0
- package/dist/office/people/business_female_04/body.webp +0 -0
- package/dist/office/people/business_female_04/head.webp +0 -0
- package/dist/office/people/business_female_04/opacity.webp +0 -0
- package/dist/office/people/business_female_04/portrait.webp +0 -0
- package/dist/office/people/business_male_01/avatar.fbx +0 -0
- package/dist/office/people/business_male_01/body.webp +0 -0
- package/dist/office/people/business_male_01/head.webp +0 -0
- package/dist/office/people/business_male_01/opacity.webp +0 -0
- package/dist/office/people/business_male_01/portrait.webp +0 -0
- package/dist/office/people/business_male_02/avatar.fbx +0 -0
- package/dist/office/people/business_male_02/body.webp +0 -0
- package/dist/office/people/business_male_02/head.webp +0 -0
- package/dist/office/people/business_male_02/opacity.webp +0 -0
- package/dist/office/people/business_male_02/portrait.webp +0 -0
- package/dist/office/people/business_male_03/avatar.fbx +0 -0
- package/dist/office/people/business_male_03/body.webp +0 -0
- package/dist/office/people/business_male_03/head.webp +0 -0
- package/dist/office/people/business_male_03/opacity.webp +0 -0
- package/dist/office/people/business_male_03/portrait.webp +0 -0
- package/dist/office/people/business_male_04/avatar.fbx +0 -0
- package/dist/office/people/business_male_04/body.webp +0 -0
- package/dist/office/people/business_male_04/head.webp +0 -0
- package/dist/office/people/business_male_04/portrait.webp +0 -0
- package/dist/office/people/business_male_05/avatar.fbx +0 -0
- package/dist/office/people/business_male_05/body.webp +0 -0
- package/dist/office/people/business_male_05/head.webp +0 -0
- package/dist/office/people/business_male_05/portrait.webp +0 -0
- package/dist/office/people/business_male_06/avatar.fbx +0 -0
- package/dist/office/people/business_male_06/body.webp +0 -0
- package/dist/office/people/business_male_06/head.webp +0 -0
- package/dist/office/people/business_male_06/portrait.webp +0 -0
- package/dist/office/people/business_male_07/avatar.fbx +0 -0
- package/dist/office/people/business_male_07/body.webp +0 -0
- package/dist/office/people/business_male_07/head.webp +0 -0
- package/dist/office/people/business_male_07/portrait.webp +0 -0
- package/dist/office/people/female_adult_02/avatar.fbx +0 -0
- package/dist/office/people/female_adult_02/body.webp +0 -0
- package/dist/office/people/female_adult_02/head.webp +0 -0
- package/dist/office/people/female_adult_02/opacity.webp +0 -0
- package/dist/office/people/female_adult_02/portrait.webp +0 -0
- package/dist/office/people/female_adult_04/avatar.fbx +0 -0
- package/dist/office/people/female_adult_04/body.webp +0 -0
- package/dist/office/people/female_adult_04/head.webp +0 -0
- package/dist/office/people/female_adult_04/opacity.webp +0 -0
- package/dist/office/people/female_adult_04/portrait.webp +0 -0
- package/dist/office/people/female_adult_05/avatar.fbx +0 -0
- package/dist/office/people/female_adult_05/body.webp +0 -0
- package/dist/office/people/female_adult_05/head.webp +0 -0
- package/dist/office/people/female_adult_05/opacity.webp +0 -0
- package/dist/office/people/female_adult_05/portrait.webp +0 -0
- package/dist/office/people/female_adult_07/avatar.fbx +0 -0
- package/dist/office/people/female_adult_07/body.webp +0 -0
- package/dist/office/people/female_adult_07/head.webp +0 -0
- package/dist/office/people/female_adult_07/opacity.webp +0 -0
- package/dist/office/people/female_adult_07/portrait.webp +0 -0
- package/dist/office/people/female_adult_09/avatar.fbx +0 -0
- package/dist/office/people/female_adult_09/body.webp +0 -0
- package/dist/office/people/female_adult_09/head.webp +0 -0
- package/dist/office/people/female_adult_09/opacity.webp +0 -0
- package/dist/office/people/female_adult_09/portrait.webp +0 -0
- package/dist/office/people/female_adult_10/avatar.fbx +0 -0
- package/dist/office/people/female_adult_10/body.webp +0 -0
- package/dist/office/people/female_adult_10/head.webp +0 -0
- package/dist/office/people/female_adult_10/opacity.webp +0 -0
- package/dist/office/people/female_adult_10/portrait.webp +0 -0
- package/dist/office/people/female_adult_13/avatar.fbx +0 -0
- package/dist/office/people/female_adult_13/body.webp +0 -0
- package/dist/office/people/female_adult_13/head.webp +0 -0
- package/dist/office/people/female_adult_13/opacity.webp +0 -0
- package/dist/office/people/female_adult_13/portrait.webp +0 -0
- package/dist/office/people/female_adult_14/avatar.fbx +0 -0
- package/dist/office/people/female_adult_14/body.webp +0 -0
- package/dist/office/people/female_adult_14/head.webp +0 -0
- package/dist/office/people/female_adult_14/opacity.webp +0 -0
- package/dist/office/people/female_adult_14/portrait.webp +0 -0
- package/dist/office/people/female_adult_15/avatar.fbx +0 -0
- package/dist/office/people/female_adult_15/body.webp +0 -0
- package/dist/office/people/female_adult_15/head.webp +0 -0
- package/dist/office/people/female_adult_15/opacity.webp +0 -0
- package/dist/office/people/female_adult_15/portrait.webp +0 -0
- package/dist/office/people/female_adult_17/avatar.fbx +0 -0
- package/dist/office/people/female_adult_17/body.webp +0 -0
- package/dist/office/people/female_adult_17/head.webp +0 -0
- package/dist/office/people/female_adult_17/opacity.webp +0 -0
- package/dist/office/people/female_adult_17/portrait.webp +0 -0
- package/dist/office/people/male_adult_02/avatar.fbx +0 -0
- package/dist/office/people/male_adult_02/body.webp +0 -0
- package/dist/office/people/male_adult_02/head.webp +0 -0
- package/dist/office/people/male_adult_02/opacity.webp +0 -0
- package/dist/office/people/male_adult_02/portrait.webp +0 -0
- package/dist/office/people/male_adult_03/avatar.fbx +0 -0
- package/dist/office/people/male_adult_03/body.webp +0 -0
- package/dist/office/people/male_adult_03/head.webp +0 -0
- package/dist/office/people/male_adult_03/opacity.webp +0 -0
- package/dist/office/people/male_adult_03/portrait.webp +0 -0
- package/dist/office/people/male_adult_04/avatar.fbx +0 -0
- package/dist/office/people/male_adult_04/body.webp +0 -0
- package/dist/office/people/male_adult_04/head.webp +0 -0
- package/dist/office/people/male_adult_04/opacity.webp +0 -0
- package/dist/office/people/male_adult_04/portrait.webp +0 -0
- package/dist/office/people/male_adult_06/avatar.fbx +0 -0
- package/dist/office/people/male_adult_06/body.webp +0 -0
- package/dist/office/people/male_adult_06/head.webp +0 -0
- package/dist/office/people/male_adult_06/opacity.webp +0 -0
- package/dist/office/people/male_adult_06/portrait.webp +0 -0
- package/dist/office/people/male_adult_08/avatar.fbx +0 -0
- package/dist/office/people/male_adult_08/body.webp +0 -0
- package/dist/office/people/male_adult_08/head.webp +0 -0
- package/dist/office/people/male_adult_08/opacity.webp +0 -0
- package/dist/office/people/male_adult_08/portrait.webp +0 -0
- package/dist/office/people/male_adult_12/avatar.fbx +0 -0
- package/dist/office/people/male_adult_12/body.webp +0 -0
- package/dist/office/people/male_adult_12/head.webp +0 -0
- package/dist/office/people/male_adult_12/portrait.webp +0 -0
- package/dist/office/people/male_adult_13/avatar.fbx +0 -0
- package/dist/office/people/male_adult_13/body.webp +0 -0
- package/dist/office/people/male_adult_13/head.webp +0 -0
- package/dist/office/people/male_adult_13/portrait.webp +0 -0
- package/dist/office/people/male_adult_14/avatar.fbx +0 -0
- package/dist/office/people/male_adult_14/body.webp +0 -0
- package/dist/office/people/male_adult_14/head.webp +0 -0
- package/dist/office/people/male_adult_14/portrait.webp +0 -0
- package/dist/office/people/male_adult_19/avatar.fbx +0 -0
- package/dist/office/people/male_adult_19/body.webp +0 -0
- package/dist/office/people/male_adult_19/head.webp +0 -0
- package/dist/office/people/male_adult_19/portrait.webp +0 -0
- package/dist/office/textures/herringbone_parquet_diff.webp +0 -0
- package/dist/office/textures/herringbone_parquet_nor.webp +0 -0
- package/dist/office/textures/herringbone_parquet_rough.webp +0 -0
- package/dist/office/textures/laminate_floor_02_diff.webp +0 -0
- package/dist/office/textures/laminate_floor_02_nor.webp +0 -0
- package/dist/office/textures/laminate_floor_02_rough.webp +0 -0
- package/dist/office/textures/oak_veneer_detail.webp +0 -0
- package/dist/office/textures/oak_veneer_nor.webp +0 -0
- package/dist/office/textures/oak_veneer_rough.webp +0 -0
- package/docs/ASSETS.md +43 -0
- package/docs/licenses/Microsoft-Rocketbox-LICENSE.txt +21 -0
- package/docs/marketing/hero.jpg +0 -0
- package/package.json +4 -3
- package/dist/office/assets/index-77PZXJ_x.js +0 -5529
- package/dist/office/assets/index-CB3kPPFD.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
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
|
+
|
|
5
34
|
## 0.1.1 (2026-10-05)
|
|
6
35
|
|
|
7
36
|
- Fix piu-piu setup failing with launchd "Bootstrap failed: 5" when the service was already loaded
|
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.
|
|
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
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **Control:** you talk to any
|
|
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
|
-
- [
|
|
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
|
|
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;
|
|
129
|
-
| `piu-piu start` · `stop` · `restart` | The background service. `stop` and `restart` refuse while
|
|
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
|
-
|
|
|
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.
|
|
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
|
|
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
|
-
##
|
|
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
|
-
| **
|
|
207
|
-
| **
|
|
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
|
-
**
|
|
211
|
-
- **When a
|
|
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
|
|
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
|
|
315
|
-
| `GET /api/specialists/:type/runs` | Recent calls of one
|
|
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
|
|
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/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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1595
|
-
if (
|
|
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,
|
|
1598
|
-
else if (!
|
|
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,
|
|
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
|
-
|
|
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;
|