agent-office-studio 0.7.0 → 0.8.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 +13 -1
- package/README.md +1 -1
- package/bin/agentoffice.js +6 -1
- package/dist/assets/index-BAvX7Imu.css +1 -0
- package/dist/assets/index-C01GpSoa.js +4999 -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/index.html +11 -2
- package/docs/phone.md +51 -3
- package/docs/security.md +8 -3
- package/package.json +4 -2
- package/server/cloudflared.js +84 -0
- package/server/economy.js +455 -0
- package/server/index.js +361 -16
- package/server/journeys.js +101 -0
- package/server/office-mcp.js +19 -0
- package/server/phone.js +70 -0
- package/server/settings.js +3 -0
- package/server/talk.js +51 -0
- package/server/tunnel.js +136 -0
- package/shared/shop.js +127 -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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.html
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
})();
|
|
26
26
|
</script>
|
|
27
27
|
<title>Office</title>
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
29
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-C01GpSoa.js"></script>
|
|
29
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BAvX7Imu.css">
|
|
30
30
|
</head>
|
|
31
31
|
<body>
|
|
32
32
|
<div class="app" id="app" data-m="office">
|
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
<button type="button" class="theme-btn" id="acctBtn" aria-label="Claude access" aria-haspopup="true" aria-expanded="false" title="Claude access">
|
|
50
50
|
<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
51
|
</button>
|
|
52
|
+
<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">
|
|
53
|
+
<svg viewBox="0 0 24 24"><rect x="7" y="3" width="10" height="18" rx="2.5"/><path d="M11 18h2"/></svg>
|
|
54
|
+
</button>
|
|
52
55
|
<button type="button" class="theme-btn" id="setBtn" aria-label="Settings" aria-haspopup="dialog" aria-expanded="false" title="Settings">
|
|
53
56
|
<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
57
|
</button>
|
|
@@ -60,6 +63,7 @@
|
|
|
60
63
|
<div class="theme-pop lb-pop" id="lbPop" role="dialog" aria-label="Leaderboard" hidden></div>
|
|
61
64
|
<div class="theme-pop link-pop" id="linkPop" role="dialog" aria-label="Linked offices" hidden></div>
|
|
62
65
|
<div class="theme-pop acct-pop" id="acctPop" role="dialog" aria-label="Claude access" hidden></div>
|
|
66
|
+
<div class="theme-pop phone-pop" id="phonePop" role="dialog" aria-label="Open on your phone" hidden></div>
|
|
63
67
|
</div>
|
|
64
68
|
<div class="level">
|
|
65
69
|
<div class="xpbar"><i id="xpbar"></i></div>
|
|
@@ -111,6 +115,7 @@
|
|
|
111
115
|
<div class="ac" id="ac" hidden></div>
|
|
112
116
|
<div class="tray" id="tray" hidden></div>
|
|
113
117
|
<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>
|
|
118
|
+
<div class="attn" id="attn" role="region" aria-label="Needs your attention" hidden></div>
|
|
114
119
|
<textarea id="input" rows="1" placeholder="Message the office · @agent #project" aria-label="Message the office"></textarea>
|
|
115
120
|
<div class="controls">
|
|
116
121
|
<button type="button" class="mic" id="mic" title="Record a voice note (hold ⌥M)" aria-label="Record a voice note">
|
|
@@ -190,6 +195,9 @@
|
|
|
190
195
|
<div id="labels"></div>
|
|
191
196
|
|
|
192
197
|
<div class="hud">
|
|
198
|
+
<button type="button" class="chip pot" id="shopBtn" aria-haspopup="dialog" aria-expanded="false" title="Shop and office pot" hidden><b>₦0</b><span>Shop</span></button>
|
|
199
|
+
<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>
|
|
200
|
+
<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
201
|
<div class="chip away" id="away" hidden></div>
|
|
194
202
|
<div class="chip" id="stats"></div>
|
|
195
203
|
<div class="chip tools">
|
|
@@ -237,5 +245,6 @@
|
|
|
237
245
|
</nav>
|
|
238
246
|
</div>
|
|
239
247
|
<dialog class="settings" id="settings" aria-label="Settings"></dialog>
|
|
248
|
+
<dialog class="settings shop" id="shop" aria-label="Shop"></dialog>
|
|
240
249
|
</body>
|
|
241
250
|
</html>
|
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.8.0",
|
|
4
4
|
"description": "A 3D office where the staff are Claude Code agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -45,10 +45,12 @@
|
|
|
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/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/wagers-server.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": {
|
|
@@ -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
|
+
}
|