agent-office-studio 0.7.0 → 0.9.1
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 +61 -1
- package/README.md +13 -1
- package/bin/agentoffice.js +6 -1
- package/dist/assets/code-Dtdckl8K.js +32 -0
- package/dist/assets/index-DxWppRAd.css +1 -0
- package/dist/assets/index-Y1a-xw5C.js +5040 -0
- package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/assets/{jetbrains-mono-cyrillic-400-normal-CIEtRijs.woff → jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff} +0 -0
- package/dist/assets/{jetbrains-mono-greek-400-normal-CNojz0t3.woff → jetbrains-mono-greek-400-normal-B9oWc5Lo.woff} +0 -0
- package/dist/assets/{jetbrains-mono-latin-400-normal-B11XCQ5g.woff → jetbrains-mono-latin-400-normal-6-qcROiO.woff} +0 -0
- package/dist/assets/{jetbrains-mono-latin-ext-400-normal-DHMOLxAi.woff → jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff} +0 -0
- package/dist/assets/{jetbrains-mono-vietnamese-400-normal-BeU3T8wf.woff → jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff} +0 -0
- package/dist/assets/pdf-BtCWNl9F.js +55 -0
- package/dist/assets/pdf-a7kwa8Fi.js +1 -0
- package/dist/assets/pdf.worker.min-BJd0dv5W.js +53 -0
- package/dist/assets/pdf.worker.min-D_0FwaWY.js +1 -0
- package/dist/index.html +32 -26
- package/dist/theme-boot.js +13 -0
- package/docs/desktop.md +52 -0
- package/docs/phone.md +51 -3
- package/docs/security.md +8 -3
- package/package.json +20 -2
- package/server/changes.js +13 -2
- package/server/chief.js +17 -0
- package/server/cloudflared.js +84 -0
- package/server/economy.js +478 -0
- package/server/files.js +104 -0
- package/server/guests.js +59 -21
- package/server/index.js +743 -59
- package/server/journeys.js +101 -0
- package/server/links.js +11 -4
- package/server/machine.js +164 -0
- package/server/office-mcp.js +29 -0
- package/server/phone.js +70 -0
- package/server/procstat.js +12 -6
- package/server/settings.js +5 -0
- package/server/talk.js +51 -0
- package/server/tunnel.js +136 -0
- package/server/visits.js +14 -4
- package/shared/shop.js +134 -0
- package/dist/assets/index-BtHSEs3B.js +0 -4475
- package/dist/assets/index-DVuFVmWF.css +0 -1
- package/dist/assets/inter-cyrillic-ext-wght-normal-B2xhLi22.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-CMZtQduZ.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-CGAr0uHJ.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CaVNZxsx.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-CFHvXkgd.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-C2S99t-D.woff2 +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function r(e){return new Worker("/assets/pdf.worker.min-BJd0dv5W.js",{name:e==null?void 0:e.name})}export{r as default};
|
package/dist/index.html
CHANGED
|
@@ -11,27 +11,14 @@
|
|
|
11
11
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
12
12
|
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
|
13
13
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
14
|
-
<script>
|
|
15
|
-
// Before first paint, so a saved theme never flashes the default. theme.js owns it after that.
|
|
16
|
-
(function () {
|
|
17
|
-
try {
|
|
18
|
-
var t = localStorage.getItem('office.theme') || 'paper';
|
|
19
|
-
var m = localStorage.getItem('office.mode') || 'auto';
|
|
20
|
-
var d = m === 'dark' || (m === 'auto' && matchMedia('(prefers-color-scheme: dark)').matches);
|
|
21
|
-
var r = document.documentElement;
|
|
22
|
-
r.dataset.theme = t;
|
|
23
|
-
r.dataset.mode = d ? 'dark' : 'light';
|
|
24
|
-
} catch (e) {}
|
|
25
|
-
})();
|
|
26
|
-
</script>
|
|
14
|
+
<script src="/theme-boot.js"></script>
|
|
27
15
|
<title>Office</title>
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
29
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-Y1a-xw5C.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DxWppRAd.css">
|
|
30
18
|
</head>
|
|
31
19
|
<body>
|
|
32
20
|
<div class="app" id="app" data-m="office">
|
|
33
21
|
<aside class="side">
|
|
34
|
-
<div class="side-grip" id="side-grip" role="separator" aria-orientation="vertical" aria-label="Resize the side panel" tabindex="0"></div>
|
|
35
22
|
<header class="brand">
|
|
36
23
|
<div class="title">
|
|
37
24
|
<span class="title-start">
|
|
@@ -49,6 +36,9 @@
|
|
|
49
36
|
<button type="button" class="theme-btn" id="acctBtn" aria-label="Claude access" aria-haspopup="true" aria-expanded="false" title="Claude access">
|
|
50
37
|
<svg viewBox="0 0 24 24"><circle cx="8" cy="15" r="4"/><path d="M10.8 12.2 20 3"/><path d="m17 6 3 3"/><path d="m14.5 8.5 2 2"/></svg>
|
|
51
38
|
</button>
|
|
39
|
+
<button type="button" class="theme-btn" id="phoneBtn" aria-label="Open on your phone" aria-haspopup="true" aria-expanded="false" title="Open on your phone">
|
|
40
|
+
<svg viewBox="0 0 24 24"><rect x="7" y="3" width="10" height="18" rx="2.5"/><path d="M11 18h2"/></svg>
|
|
41
|
+
</button>
|
|
52
42
|
<button type="button" class="theme-btn" id="setBtn" aria-label="Settings" aria-haspopup="dialog" aria-expanded="false" title="Settings">
|
|
53
43
|
<svg viewBox="0 0 24 24"><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></svg>
|
|
54
44
|
</button>
|
|
@@ -60,6 +50,7 @@
|
|
|
60
50
|
<div class="theme-pop lb-pop" id="lbPop" role="dialog" aria-label="Leaderboard" hidden></div>
|
|
61
51
|
<div class="theme-pop link-pop" id="linkPop" role="dialog" aria-label="Linked offices" hidden></div>
|
|
62
52
|
<div class="theme-pop acct-pop" id="acctPop" role="dialog" aria-label="Claude access" hidden></div>
|
|
53
|
+
<div class="theme-pop phone-pop" id="phonePop" role="dialog" aria-label="Open on your phone" hidden></div>
|
|
63
54
|
</div>
|
|
64
55
|
<div class="level">
|
|
65
56
|
<div class="xpbar"><i id="xpbar"></i></div>
|
|
@@ -102,6 +93,7 @@
|
|
|
102
93
|
<button type="button" data-tab="jobs">Jobs <span id="jobCount"></span></button>
|
|
103
94
|
<button type="button" data-tab="projects">Projects</button>
|
|
104
95
|
<button type="button" data-tab="team">Team</button>
|
|
96
|
+
<button type="button" data-tab="needs">Needs you <span id="needsCount"></span></button>
|
|
105
97
|
</nav>
|
|
106
98
|
<section class="pane" id="chatPane">
|
|
107
99
|
<div class="channels" id="channels" role="tablist" aria-label="Channels"></div>
|
|
@@ -111,6 +103,7 @@
|
|
|
111
103
|
<div class="ac" id="ac" hidden></div>
|
|
112
104
|
<div class="tray" id="tray" hidden></div>
|
|
113
105
|
<div class="rec" id="rec" hidden><i></i><span id="recText">Recording</span><button type="button" id="recStop" class="link">Stop</button><button type="button" id="recDrop" class="link">Discard</button></div>
|
|
106
|
+
<div class="attn" id="attn" role="region" aria-label="Needs your attention" hidden></div>
|
|
114
107
|
<textarea id="input" rows="1" placeholder="Message the office · @agent #project" aria-label="Message the office"></textarea>
|
|
115
108
|
<div class="controls">
|
|
116
109
|
<button type="button" class="mic" id="mic" title="Record a voice note (hold ⌥M)" aria-label="Record a voice note">
|
|
@@ -179,6 +172,9 @@
|
|
|
179
172
|
</div>
|
|
180
173
|
<div class="projects" id="projects"></div>
|
|
181
174
|
</section>
|
|
175
|
+
<section class="pane" id="needsPane" hidden>
|
|
176
|
+
<div class="jobs needs" id="needs"></div>
|
|
177
|
+
</section>
|
|
182
178
|
<section class="pane" id="jobsPane" hidden>
|
|
183
179
|
<div class="jobs" id="jobs"></div>
|
|
184
180
|
<button class="ghost" id="clear" hidden>Clear finished</button>
|
|
@@ -190,6 +186,9 @@
|
|
|
190
186
|
<div id="labels"></div>
|
|
191
187
|
|
|
192
188
|
<div class="hud">
|
|
189
|
+
<button type="button" class="chip pot" id="shopBtn" aria-haspopup="dialog" aria-expanded="false" aria-label="Shop and office pot" title="Shop and office pot" hidden><b>₦0</b><span>Shop</span></button>
|
|
190
|
+
<button type="button" class="chip pot shop-door" data-shop-tab="rich" aria-haspopup="dialog" aria-label="Forbes list: the richest agents" title="Forbes list: the richest agents" hidden><i aria-hidden="true">👑</i><span>Forbes list</span></button>
|
|
191
|
+
<button type="button" class="chip pot shop-door" data-shop-tab="looks" aria-haspopup="dialog" aria-label="Customise the office" title="Customise the office: layout, paint, floor and furniture" hidden><i aria-hidden="true">🎨</i><span>Customise</span></button>
|
|
193
192
|
<div class="chip away" id="away" hidden></div>
|
|
194
193
|
<div class="chip" id="stats"></div>
|
|
195
194
|
<div class="chip tools">
|
|
@@ -202,6 +201,9 @@
|
|
|
202
201
|
<button type="button" id="recenter" title="Recenter (R)" aria-label="Recenter">
|
|
203
202
|
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>
|
|
204
203
|
</button>
|
|
204
|
+
<button type="button" id="arrange" aria-pressed="false" title="Arrange the office: move desks, tables and furniture" aria-label="Arrange the office">
|
|
205
|
+
<svg viewBox="0 0 24 24"><path d="M12 2v20M2 12h20M12 2l-3 3M12 2l3 3M12 22l-3-3M12 22l3-3M2 12l3-3M2 12l3 3M22 12l-3-3M22 12l-3 3"/></svg>
|
|
206
|
+
</button>
|
|
205
207
|
<button type="button" id="standup" title="Call a standup" aria-label="Call a standup">
|
|
206
208
|
<svg viewBox="0 0 24 24"><circle cx="12" cy="5" r="2"/><circle cx="5" cy="15" r="2"/><circle cx="19" cy="15" r="2"/><path d="M8.5 20.5a7.5 7.5 0 0 0 7 0M3.5 10.5a8.5 8.5 0 0 1 4.5-5M20.5 10.5a8.5 8.5 0 0 0-4.5-5"/></svg>
|
|
207
209
|
</button>
|
|
@@ -219,23 +221,27 @@
|
|
|
219
221
|
<aside class="detail" id="detail" hidden></aside>
|
|
220
222
|
|
|
221
223
|
<nav class="mnav" id="mnav" aria-label="Sections">
|
|
222
|
-
<button type="button" data-m="office" class="on">
|
|
223
|
-
<svg viewBox="0 0 24 24"><path d="M3 10.5L12 4l9 6.5"/><path d="M5 9.5V20h14V9.5"/><path d="M10 20v-5h4v5"/></svg>Office
|
|
224
|
+
<button type="button" data-m="office" title="Office" aria-label="Office" class="on">
|
|
225
|
+
<svg viewBox="0 0 24 24"><path d="M3 10.5L12 4l9 6.5"/><path d="M5 9.5V20h14V9.5"/><path d="M10 20v-5h4v5"/></svg><span class="lbl">Office</span>
|
|
226
|
+
</button>
|
|
227
|
+
<button type="button" data-m="chat" title="Chat" aria-label="Chat">
|
|
228
|
+
<svg viewBox="0 0 24 24"><path d="M4 5h16v11H9l-5 4z"/></svg><span class="lbl">Chat</span><i class="dot" id="mChatDot" hidden></i>
|
|
224
229
|
</button>
|
|
225
|
-
<button type="button" data-m="
|
|
226
|
-
<svg viewBox="0 0 24 24"><path d="M4
|
|
230
|
+
<button type="button" data-m="jobs" title="Jobs" aria-label="Jobs">
|
|
231
|
+
<svg viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg><span class="lbl">Jobs</span><b id="mJobCount"></b>
|
|
227
232
|
</button>
|
|
228
|
-
<button type="button" data-m="
|
|
229
|
-
<svg viewBox="0 0 24 24"><path d="
|
|
233
|
+
<button type="button" data-m="projects" title="Projects" aria-label="Projects">
|
|
234
|
+
<svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="lbl">Projects</span>
|
|
230
235
|
</button>
|
|
231
|
-
<button type="button" data-m="
|
|
232
|
-
<svg viewBox="0 0 24 24"><path d="M3
|
|
236
|
+
<button type="button" data-m="team" title="Team" aria-label="Team">
|
|
237
|
+
<svg viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2"/><path d="M3 20a6 6 0 0 1 12 0"/><circle cx="17" cy="9" r="2.4"/><path d="M17 14.5a4.6 4.6 0 0 1 4 5.5"/></svg><span class="lbl">Team</span>
|
|
233
238
|
</button>
|
|
234
|
-
<button type="button" data-m="
|
|
235
|
-
<svg viewBox="0 0 24 24"><
|
|
239
|
+
<button type="button" data-m="needs" title="Needs you" aria-label="Needs you">
|
|
240
|
+
<svg viewBox="0 0 24 24"><path d="M6 17V11a6 6 0 0 1 12 0v6l1.5 2h-15z"/><path d="M10 21h4"/></svg><span class="lbl">Needs you</span><b id="mNeedsCount"></b>
|
|
236
241
|
</button>
|
|
237
242
|
</nav>
|
|
238
243
|
</div>
|
|
239
244
|
<dialog class="settings" id="settings" aria-label="Settings"></dialog>
|
|
245
|
+
<dialog class="settings shop" id="shop" aria-label="Shop"></dialog>
|
|
240
246
|
</body>
|
|
241
247
|
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Before first paint, so a saved theme never flashes the default. theme.js owns it after that.
|
|
2
|
+
// A file rather than inline, so the page's CSP can be script-src 'self'.
|
|
3
|
+
(function () {
|
|
4
|
+
try {
|
|
5
|
+
var t = localStorage.getItem('office.theme') || 'paper';
|
|
6
|
+
var m = localStorage.getItem('office.mode') || 'auto';
|
|
7
|
+
var d = m === 'dark' || (m === 'auto' && matchMedia('(prefers-color-scheme: dark)').matches);
|
|
8
|
+
var r = document.documentElement;
|
|
9
|
+
r.dataset.theme = t;
|
|
10
|
+
r.dataset.mode = d ? 'dark' : 'light';
|
|
11
|
+
r.dataset.nav = localStorage.getItem('office.nav') === 'sidebar' ? 'sidebar' : 'dock';
|
|
12
|
+
} catch (e) {}
|
|
13
|
+
})();
|
package/docs/desktop.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Desktop app (Tauri)
|
|
2
|
+
|
|
3
|
+
The desktop app is a thin Tauri shell around the same office server and web UI you get from `agentoffice`. The shell lives in `desktop/`; this page covers how the web side behaves inside it.
|
|
4
|
+
|
|
5
|
+
## How the shell starts the office
|
|
6
|
+
|
|
7
|
+
1. Binds `127.0.0.1:0` to get a free port `N`.
|
|
8
|
+
2. Runs `$SHELL -lc 'exec node <Resources>/app/bin/agentoffice.js --port N'`. The login shell means a Finder-launched app still finds `node` and `claude` on `PATH`.
|
|
9
|
+
3. Sets `OFFICE_ROOT` (the folder you pick on first run, remembered), leaves `OFFICE_DATA` at `~/.agent-office`, and sets `OFFICE_DESKTOP=1`.
|
|
10
|
+
4. Waits until `N` accepts connections, reads `~/.agent-office/office-key`, and points the window at `http://127.0.0.1:N/?key=…`.
|
|
11
|
+
5. On quit, sends `SIGTERM` to the server and waits, so it flushes its state.
|
|
12
|
+
|
|
13
|
+
The server needs no desktop-specific behaviour: the phone QR code only prints with `--phone`, which the shell never passes.
|
|
14
|
+
|
|
15
|
+
## External links
|
|
16
|
+
|
|
17
|
+
The webview drops `target="_blank"` links and `window.open`. `web/src/desktop.js` detects `window.__TAURI__` and sends those URLs to the opener plugin (`window.__TAURI__.opener.openUrl`). It handles:
|
|
18
|
+
|
|
19
|
+
- links to other sites (`https:`, `http:`, `mailto:`),
|
|
20
|
+
- `window.open(url)`.
|
|
21
|
+
|
|
22
|
+
It also adds a `desktop` class to `<html>` so CSS can adapt. In a normal browser the module does nothing.
|
|
23
|
+
|
|
24
|
+
Links to the office itself are left alone: the system browser doesn't have the webview's HttpOnly key cookie and would get a 401, and the key is stripped from the page URL so it can't be appended.
|
|
25
|
+
|
|
26
|
+
The shell must enable the opener plugin with a capability that allows the localhost page to call it.
|
|
27
|
+
|
|
28
|
+
## Building it
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
cd desktop && npm install
|
|
32
|
+
npm run dev # runs the repo; needs a built dist/
|
|
33
|
+
npm run build # stages from HEAD, writes src-tauri/target/release/bundle/{macos,dmg}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Rust comes via rustup. The build log goes to `~/.agent-office/desktop.log`.
|
|
37
|
+
|
|
38
|
+
## Opening a downloaded build
|
|
39
|
+
|
|
40
|
+
The dmg is signed ad hoc (`signingIdentity: "-"` in `tauri.conf.json`) and is **not notarized**: that needs an Apple *Developer ID Application* certificate and an Apple notarization login, which the project doesn't have. So on another Mac, Gatekeeper blocks the first launch of a downloaded copy.
|
|
41
|
+
|
|
42
|
+
Before the ad-hoc signing was added, the bundle carried only the linker's signature on the executable, with no sealed resources. Gatekeeper treats that as a broken signature and says "Agent Office is damaged and can't be opened". Now the signature is valid, and the message is the milder "can't be opened because Apple cannot check it for malicious software".
|
|
43
|
+
|
|
44
|
+
For users, any one of:
|
|
45
|
+
|
|
46
|
+
- Terminal: `xattr -cr "/Applications/Agent Office.app"`, then open it (drag it to Applications first). This strips the download quarantine flag.
|
|
47
|
+
- Right-click the app, choose Open, then Open again.
|
|
48
|
+
- macOS 15 and later: try to open it once, then System Settings, Privacy & Security, Open Anyway.
|
|
49
|
+
|
|
50
|
+
To remove the warning properly: enrol in the Apple Developer Program, create a Developer ID Application certificate, then build with `APPLE_SIGNING_IDENTITY="Developer ID Application: …"` and either `APPLE_ID`, `APPLE_PASSWORD` (app-specific) and `APPLE_TEAM_ID`, or `APPLE_API_KEY`, `APPLE_API_ISSUER` and `APPLE_API_KEY_PATH`. Tauri then signs, notarizes and staples the dmg. Those variables take precedence over the ad-hoc identity.
|
|
51
|
+
|
|
52
|
+
The dmg is arm64 only (Apple Silicon).
|
package/docs/phone.md
CHANGED
|
@@ -8,10 +8,58 @@ for an office in its own window.
|
|
|
8
8
|
The app shell is cached so it opens when the connection drops; live office
|
|
9
9
|
state never is, because a stale office is worse than none.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## From anywhere: a public address
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
The
|
|
13
|
+
Press the phone button at the top of the side panel, then **Make a public
|
|
14
|
+
address**. The office starts a [Cloudflare quick tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/)
|
|
15
|
+
(no account), and shows a QR code for a random `https://….trycloudflare.com`
|
|
16
|
+
address with the office key on the end. Scan it from any network, mobile data
|
|
17
|
+
included. Nothing on your machine starts listening, and the tunnel goes out
|
|
18
|
+
from your machine, so no router settings change.
|
|
19
|
+
|
|
20
|
+
There is nothing to install. The office uses `cloudflared` if it is on your
|
|
21
|
+
PATH, and otherwise downloads a pinned release (about 20–55 MB, once) from
|
|
22
|
+
Cloudflare's GitHub into the data folder, checked against its SHA-256 before it
|
|
23
|
+
runs. macOS, Linux (x64, arm64, arm) and Windows (x64, x86). Any
|
|
24
|
+
`~/.cloudflared/config.yml` you have for your own tunnels is left alone and
|
|
25
|
+
ignored.
|
|
26
|
+
|
|
27
|
+
The QR code only appears once the office has reached itself through the new
|
|
28
|
+
address from the internet, which usually takes about a minute: a phone that
|
|
29
|
+
looks up a brand-new name too early can remember it as missing for a while.
|
|
30
|
+
If the address never works (a network that blocks Cloudflare's tunnel ports,
|
|
31
|
+
for instance), the button says so instead of handing you a blank page.
|
|
32
|
+
|
|
33
|
+
It is off until you press the button, closes when you press it again or the
|
|
34
|
+
office stops, and the address is different every time. The key is what protects
|
|
35
|
+
it: 64 random hex digits, over https. Treat the link like a password. Whoever
|
|
36
|
+
has it can run Claude in your projects with the clearance you give it. To sign
|
|
37
|
+
every browser out, delete `data/office-key` and restart.
|
|
38
|
+
|
|
39
|
+
## Scan a QR code (same Wi-Fi)
|
|
40
|
+
|
|
41
|
+
The quickest way, for a phone on the same Wi-Fi as this machine:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
agentoffice --phone
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That also listens on your local network, and prints a QR code in the terminal
|
|
48
|
+
after the usual link. Scan it with the phone's camera and the office opens, key
|
|
49
|
+
included. The same code is in the office itself, under the phone button at the
|
|
50
|
+
top of the side panel. `OFFICE_LAN=1` does what `--phone` does.
|
|
51
|
+
|
|
52
|
+
Only this machine's own private addresses (`192.168.x.x`, `10.x.x.x`,
|
|
53
|
+
`172.16-31.x.x`) are accepted as the address, and the page still needs the key.
|
|
54
|
+
But it is plain `http`, and the link carries the key, so anyone who can watch
|
|
55
|
+
that network's traffic could copy it and run Claude in your projects. Use it at
|
|
56
|
+
home, never on a café, hotel or office network; elsewhere use Tailscale below.
|
|
57
|
+
With `OFFICE_ALLOWED_HOSTS` set, the QR points at that name instead.
|
|
58
|
+
|
|
59
|
+
## Reaching your machine from anywhere
|
|
60
|
+
|
|
61
|
+
By default the server only listens on `127.0.0.1`, so a phone can't reach it
|
|
62
|
+
directly. Away from home, the supported way is [Tailscale](https://tailscale.com): put the office behind
|
|
15
63
|
`tailscale serve` on this machine, then tell the office to accept that name.
|
|
16
64
|
|
|
17
65
|
```bash
|
package/docs/security.md
CHANGED
|
@@ -5,7 +5,9 @@ hand out a shell. It is built to be used by one person, on their own machine.
|
|
|
5
5
|
|
|
6
6
|
## Who can reach it
|
|
7
7
|
|
|
8
|
-
- The server listens on `127.0.0.1` only
|
|
8
|
+
- The server listens on `127.0.0.1` only, unless you start it with `--phone`
|
|
9
|
+
(`OFFICE_LAN=1`), which also listens on your local network for a phone to
|
|
10
|
+
scan. See [On your phone](phone.md).
|
|
9
11
|
- Every request's `Host` and `Origin` must be local (or a name in
|
|
10
12
|
`OFFICE_ALLOWED_HOSTS`), which keeps other websites and DNS-rebinding
|
|
11
13
|
attacks out of it. A page on another localhost port counts as another
|
|
@@ -19,8 +21,11 @@ hand out a shell. It is built to be used by one person, on their own machine.
|
|
|
19
21
|
- Agents call back into the office (the roster, messages, hand-offs) with a
|
|
20
22
|
per-job token that dies with the job.
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
The office never opens itself to the internet. The public address on the phone
|
|
25
|
+
button is yours to turn on and off; while it is on, the key in the link is the
|
|
26
|
+
only thing between the internet and your projects, so keep it private and turn
|
|
27
|
+
it off when you're done. Don't put the office behind any other public URL. See
|
|
28
|
+
[On your phone](phone.md).
|
|
24
29
|
|
|
25
30
|
## What jobs may do
|
|
26
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-office-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "A 3D office where the staff are Claude Code agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -45,17 +45,35 @@
|
|
|
45
45
|
"start": "vite build && node server/supervise.js",
|
|
46
46
|
"voice:model": "mkdir -p data/whisper && curl -L --progress-bar -o data/whisper/ggml-base.en.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin",
|
|
47
47
|
"voice:piper": "python3 -m venv data/piper/venv && data/piper/venv/bin/pip install -q piper-tts && data/piper/venv/bin/python -m piper.download_voices --data-dir data/piper/voices en_GB-cori-high en_US-amy-medium en_GB-alan-medium en_US-kristin-medium en_US-ryan-high en_GB-alba-medium en_GB-aru-medium en_US-john-medium en_US-hfc_female-medium",
|
|
48
|
-
"test": "node scripts/smoke.mjs && node scripts/guests.mjs && node scripts/remote.mjs && node scripts/lent-home.mjs && node scripts/visits.mjs",
|
|
48
|
+
"test": "node scripts/economy.mjs && node scripts/wagers.mjs && node scripts/smoke.mjs && node scripts/machine.mjs && node scripts/talk.mjs && node scripts/guests.mjs && node scripts/remote.mjs && node scripts/lent-home.mjs && node scripts/visits.mjs && node scripts/phone.mjs && node scripts/desktop-gate.mjs && node scripts/hardening-gate.mjs && node scripts/guest-gate.mjs && node scripts/wagers-server.mjs && node scripts/chief-gate.mjs && node scripts/files.mjs",
|
|
49
49
|
"prepack": "vite build"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
+
"@dagrejs/dagre": "^1.1.8",
|
|
53
|
+
"qrcode-generator": "2.0.4",
|
|
52
54
|
"ws": "8.22.0"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
57
|
+
"@codemirror/autocomplete": "6.20.3",
|
|
58
|
+
"@codemirror/commands": "6.11.1",
|
|
59
|
+
"@codemirror/lang-css": "6.3.1",
|
|
60
|
+
"@codemirror/lang-html": "6.4.12",
|
|
61
|
+
"@codemirror/lang-javascript": "6.2.5",
|
|
62
|
+
"@codemirror/lang-json": "6.0.2",
|
|
63
|
+
"@codemirror/lang-markdown": "6.5.2",
|
|
64
|
+
"@codemirror/lang-python": "6.2.1",
|
|
65
|
+
"@codemirror/lang-yaml": "6.1.3",
|
|
66
|
+
"@codemirror/language": "6.13.1",
|
|
67
|
+
"@codemirror/search": "6.7.2",
|
|
68
|
+
"@codemirror/state": "6.7.6",
|
|
69
|
+
"@codemirror/view": "6.43.14",
|
|
55
70
|
"@fontsource-variable/inter": "5.3.0",
|
|
56
71
|
"@fontsource/jetbrains-mono": "5.3.0",
|
|
72
|
+
"@lezer/highlight": "1.2.5",
|
|
73
|
+
"codemirror": "6.0.2",
|
|
57
74
|
"concurrently": "10.0.6",
|
|
58
75
|
"marked": "^18.1.0",
|
|
76
|
+
"pdfjs-dist": "6.4.299",
|
|
59
77
|
"three": "0.186.1",
|
|
60
78
|
"vite": "6.4.3"
|
|
61
79
|
}
|
package/server/changes.js
CHANGED
|
@@ -18,6 +18,16 @@ const git = (cwd, args, env = {}) => new Promise((resolve, reject) => {
|
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
// Clean and smudge filters are commands a project's config names, and
|
|
22
|
+
// `add -A`, `write-tree` or a checkout would run them. These blank every one
|
|
23
|
+
// configured, for one call: git then passes the content through as it is.
|
|
24
|
+
export async function noFilters(cwd) {
|
|
25
|
+
let out = '';
|
|
26
|
+
try { out = await git(cwd, ['config', '--name-only', '--get-regexp', '^filter\\.']); } catch { return []; }
|
|
27
|
+
const names = new Set(out.split('\n').map((k) => k.match(/^filter\.(.+)\.[^.]+$/)?.[1]).filter(Boolean));
|
|
28
|
+
return [...names].flatMap((n) => [...['clean', 'smudge', 'process'].map((k) => `filter.${n}.${k}=`), `filter.${n}.required=false`].flatMap((c) => ['-c', c]));
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
// A tree sha for the working tree as it is right now, or null outside a repo.
|
|
22
32
|
export async function snapshotTree(cwd) {
|
|
23
33
|
const tmp = path.join(os.tmpdir(), `office-index-${process.pid}-${Math.random().toString(36).slice(2)}`);
|
|
@@ -26,8 +36,9 @@ export async function snapshotTree(cwd) {
|
|
|
26
36
|
// Seeding from the real index keeps `add -A` quick on a big tree.
|
|
27
37
|
try { fs.copyFileSync(real, tmp); } catch {}
|
|
28
38
|
const env = { GIT_INDEX_FILE: tmp };
|
|
29
|
-
await
|
|
30
|
-
|
|
39
|
+
const nf = await noFilters(cwd);
|
|
40
|
+
await git(cwd, [...nf, 'add', '-A'], env);
|
|
41
|
+
return (await git(cwd, [...nf, 'write-tree'], env)).trim();
|
|
31
42
|
} catch {
|
|
32
43
|
return null;
|
|
33
44
|
} finally {
|
package/server/chief.js
CHANGED
|
@@ -32,6 +32,22 @@ const SCHEMA = {
|
|
|
32
32
|
brief: { type: 'string' },
|
|
33
33
|
model: { type: 'string', enum: ['haiku', 'sonnet', 'opus'] },
|
|
34
34
|
escalate: { type: 'boolean' },
|
|
35
|
+
// More than one piece of new work from a single message, one per job.
|
|
36
|
+
jobs: {
|
|
37
|
+
type: 'array',
|
|
38
|
+
items: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
agentId: { type: 'string' },
|
|
42
|
+
repo: { type: 'string' },
|
|
43
|
+
brief: { type: 'string' },
|
|
44
|
+
model: { type: 'string', enum: ['haiku', 'sonnet', 'opus'] },
|
|
45
|
+
say: { type: 'string' },
|
|
46
|
+
},
|
|
47
|
+
required: ['repo', 'brief'],
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
35
51
|
say: { type: 'string' },
|
|
36
52
|
},
|
|
37
53
|
required: ['action', 'say'],
|
|
@@ -43,6 +59,7 @@ const SYSTEM = `You are the chief of staff in an office of coding agents. The bo
|
|
|
43
59
|
Actions:
|
|
44
60
|
- "followup": the message adds to, corrects, or asks about a job that already exists (working, queued or recently finished). Give its jobId. Prefer this whenever the message plausibly relates to an open job, especially the job of an agent the boss tagged. Set "escalate": true only when the boss is saying a finished job's work is wrong, incomplete, or that the agent missed the point — the second go then runs on a stronger model.
|
|
45
61
|
- "new": a new piece of work. Give "repo" (exactly one name from Projects), "brief" (the task in the boss's own words, plus any context from recent chat that the agent needs; never add requirements), "agentId" (the tagged agent's id; otherwise a free agent whose role in Staff clearly fits the work; otherwise "" for whoever is free), and "model" (see Sizing the work). Never pick a busy or dismissed agent unless the boss tagged them.
|
|
62
|
+
You can hand out several jobs from one message. When the boss tags more than one agent for work, or asks for separate pieces of work, give "jobs" instead: one entry per job, each with its own "repo", "brief", "agentId", "model" and a short "say" line for that job. Every tagged agent gets a job: the same brief for each when the boss wants them all on it (e.g. two agents doing a review pass), or a part each when the work splits naturally. Never tell the boss you can only hand out one job at a time.
|
|
46
63
|
- "dismiss": the boss wants agents sent home (dismissed, let go for now, off the floor). Give "agentIds": the ids of exactly who they mean, worked out from Staff (e.g. "everyone who's free except Mira" means every free agent but Mira). Only free agents can go; leave out anyone working.
|
|
47
64
|
- "recall": the boss wants dismissed agents back in. Give "agentIds" (every dismissed agent if they say "everyone").
|
|
48
65
|
- "clear": the boss wants agents to clear their context (start fresh, forget the conversation so far). Do it yourself; never hand it out as a job. Give "agentIds" for the agents they name, or leave it empty when they mean everyone.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Where the office gets `cloudflared` for the phone's public address, so that
|
|
2
|
+
// nobody has to install it: the one on the PATH if there is one, otherwise a
|
|
3
|
+
// pinned release downloaded once from Cloudflare's GitHub into the data
|
|
4
|
+
// folder and checked against the checksum below before it is ever run.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
export const VERSION = '2026.10.0';
|
|
12
|
+
export const BASE = `https://github.com/cloudflare/cloudflared/releases/download/${VERSION}`;
|
|
13
|
+
// Checked against the downloads themselves. For macOS the release notes give
|
|
14
|
+
// the binary inside the .tgz (`inner`); `sha256` is always the file fetched,
|
|
15
|
+
// and matches GitHub's own digest for it.
|
|
16
|
+
export const ASSETS = {
|
|
17
|
+
'darwin-arm64': { name: 'cloudflared-darwin-arm64.tgz', sha256: 'a2f79ff7b9420aa537d74af239f376da170bbabeb529aec416002adac6a72e70', inner: '72edfd3eea463aef4d5cb89e2e209cecb048cc756c2b01915de2e0ad7cb39830' },
|
|
18
|
+
'darwin-x64': { name: 'cloudflared-darwin-amd64.tgz', sha256: '903845b81828c8cb3c5d13d816a2de71c06a3da5785469df8eb0e1b736d92f9f', inner: '0560c9ab7281ac3f746055323623ed23bc0405b6dab9400474020cba33a978da' },
|
|
19
|
+
'linux-x64': { name: 'cloudflared-linux-amd64', sha256: 'd33ff2d14475178d2012c2c56beba87389ac5ded27649519f198a7d3134a99db' },
|
|
20
|
+
'linux-arm64': { name: 'cloudflared-linux-arm64', sha256: 'e6422b9d4f72d3194bc5a38676f13667c06666523217b842a877d72a80b5ac08' },
|
|
21
|
+
'linux-arm': { name: 'cloudflared-linux-arm', sha256: '1dbe8e4ec17e74bb7f49cf91db6a4903bd0f9fe41984556c7503e40b765fd099' },
|
|
22
|
+
'win32-x64': { name: 'cloudflared-windows-amd64.exe', sha256: '86aee4017b26625cee8484c113558f48effa4cd47f7aa05fcf425604e5d2b23c' },
|
|
23
|
+
'win32-ia32': { name: 'cloudflared-windows-386.exe', sha256: '0630a8779e9823a1a3b091698b8e71874e0f7b205559219f52fdd301466b5546' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const exe = (name) => (process.platform === 'win32' ? `${name}.exe` : name);
|
|
27
|
+
|
|
28
|
+
function onPath() {
|
|
29
|
+
for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
|
|
30
|
+
if (!dir) continue;
|
|
31
|
+
const f = path.join(dir, exe('cloudflared'));
|
|
32
|
+
try { fs.accessSync(f, fs.constants.X_OK); if (fs.statSync(f).isFile()) return f; } catch {}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sha = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
|
|
38
|
+
|
|
39
|
+
// Fetch url to dest, refusing it unless its SHA-256 is sha256. A .tgz is
|
|
40
|
+
// unpacked and its cloudflared kept, which must match inner. Nothing
|
|
41
|
+
// half-written is left at dest.
|
|
42
|
+
export async function download({ url, sha256, inner, dest, tgz = false }) {
|
|
43
|
+
const res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(10 * 60 * 1000) });
|
|
44
|
+
if (!res.ok) throw new Error(`Couldn’t download cloudflared (${res.status}).`);
|
|
45
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
46
|
+
if (sha(buf) !== sha256) throw new Error('The cloudflared download didn’t match its checksum, so it wasn’t used.');
|
|
47
|
+
const dir = path.dirname(dest);
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
const work = fs.mkdtempSync(path.join(dir, '.dl-'));
|
|
50
|
+
try {
|
|
51
|
+
let bin;
|
|
52
|
+
if (tgz) {
|
|
53
|
+
const archive = path.join(work, 'cf.tgz');
|
|
54
|
+
fs.writeFileSync(archive, buf);
|
|
55
|
+
const r = spawnSync('tar', ['-xzf', archive, '-C', work], { stdio: 'ignore' });
|
|
56
|
+
bin = path.join(work, 'cloudflared');
|
|
57
|
+
if (r.status !== 0 || !fs.existsSync(bin)) throw new Error('Couldn’t unpack cloudflared.');
|
|
58
|
+
if (sha(fs.readFileSync(bin)) !== inner) throw new Error('The cloudflared download didn’t match its checksum, so it wasn’t used.');
|
|
59
|
+
} else {
|
|
60
|
+
bin = path.join(work, 'bin');
|
|
61
|
+
fs.writeFileSync(bin, buf);
|
|
62
|
+
}
|
|
63
|
+
fs.chmodSync(bin, 0o755);
|
|
64
|
+
fs.renameSync(bin, dest);
|
|
65
|
+
} finally {
|
|
66
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
return dest;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let pending = null;
|
|
72
|
+
// The cloudflared to run. onStep('download') says a download is starting.
|
|
73
|
+
export function findCloudflared({ dataDir, onStep = () => {} }) {
|
|
74
|
+
if (process.env.OFFICE_CLOUDFLARED) return Promise.resolve(process.env.OFFICE_CLOUDFLARED);
|
|
75
|
+
const found = onPath();
|
|
76
|
+
if (found) return Promise.resolve(found);
|
|
77
|
+
const dest = path.join(dataDir, 'bin', exe(`cloudflared-${VERSION}`));
|
|
78
|
+
if (fs.existsSync(dest)) return Promise.resolve(dest);
|
|
79
|
+
const asset = ASSETS[`${process.platform}-${process.arch}`];
|
|
80
|
+
if (!asset) return Promise.reject(new Error(`There’s no cloudflared for ${process.platform}-${process.arch} to download. Install it yourself and try again.`));
|
|
81
|
+
onStep('download');
|
|
82
|
+
pending ||= download({ url: `${BASE}/${asset.name}`, sha256: asset.sha256, inner: asset.inner, dest, tgz: asset.name.endsWith('.tgz') }).finally(() => { pending = null; });
|
|
83
|
+
return pending;
|
|
84
|
+
}
|