agent-office-studio 0.4.0 → 0.5.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 +12 -0
- package/README.md +0 -1
- package/dist/assets/index-Cdivbl1d.css +1 -0
- package/dist/assets/{index-S_6NhxZ0.js → index-CfqwI0KM.js} +260 -255
- package/dist/index.html +10 -23
- package/docs/README.md +0 -1
- package/docs/claude-access.md +1 -2
- package/docs/configuration.md +1 -5
- package/docs/getting-started.md +0 -1
- package/docs/guides/troubleshooting.md +0 -10
- package/docs/guides/updating.md +0 -2
- package/docs/how-it-works.md +0 -2
- package/docs/linked-offices.md +1 -1
- package/docs/packaging.md +3 -4
- package/docs/phone.md +1 -2
- package/docs/security.md +1 -7
- package/docs/using-the-office.md +1 -1
- package/package.json +1 -6
- package/server/icons.js +82 -0
- package/server/index.js +11 -25
- package/dist/assets/_commonjsHelpers-CqkleIqs.js +0 -1
- package/dist/assets/addon-fit-BwprjgOP.js +0 -1
- package/dist/assets/index-CiEWLydu.css +0 -32
- package/dist/assets/xterm-C7kOH91j.js +0 -9
- package/docs/terminal.md +0 -46
- package/server/terminal.js +0 -255
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-CfqwI0KM.js"></script>
|
|
29
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cdivbl1d.css">
|
|
30
30
|
</head>
|
|
31
31
|
<body>
|
|
32
32
|
<div class="app" id="app" data-m="office">
|
|
@@ -166,9 +166,17 @@
|
|
|
166
166
|
</form>
|
|
167
167
|
</section>
|
|
168
168
|
<section class="pane" id="teamPane" hidden>
|
|
169
|
+
<div class="viewbar" role="group" aria-label="View as">
|
|
170
|
+
<button type="button" data-view="list" aria-pressed="true" aria-label="List" title="List"><svg viewBox="0 0 24 24"><path d="M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01"/></svg></button>
|
|
171
|
+
<button type="button" data-view="icons" aria-pressed="false" aria-label="Icons" title="Icons"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="6" height="6" rx="1.5"/><rect x="14" y="4" width="6" height="6" rx="1.5"/><rect x="4" y="14" width="6" height="6" rx="1.5"/><rect x="14" y="14" width="6" height="6" rx="1.5"/></svg></button>
|
|
172
|
+
</div>
|
|
169
173
|
<div class="team" id="team"></div>
|
|
170
174
|
</section>
|
|
171
175
|
<section class="pane" id="projectsPane" hidden>
|
|
176
|
+
<div class="viewbar" role="group" aria-label="View as">
|
|
177
|
+
<button type="button" data-view="list" aria-pressed="true" aria-label="List" title="List"><svg viewBox="0 0 24 24"><path d="M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01"/></svg></button>
|
|
178
|
+
<button type="button" data-view="icons" aria-pressed="false" aria-label="Icons" title="Icons"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="6" height="6" rx="1.5"/><rect x="14" y="4" width="6" height="6" rx="1.5"/><rect x="4" y="14" width="6" height="6" rx="1.5"/><rect x="14" y="14" width="6" height="6" rx="1.5"/></svg></button>
|
|
179
|
+
</div>
|
|
172
180
|
<div class="projects" id="projects"></div>
|
|
173
181
|
</section>
|
|
174
182
|
<section class="pane" id="jobsPane" hidden>
|
|
@@ -197,9 +205,6 @@
|
|
|
197
205
|
<button type="button" id="standup" title="Call a standup" aria-label="Call a standup">
|
|
198
206
|
<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>
|
|
199
207
|
</button>
|
|
200
|
-
<button type="button" id="termToggle" title="Terminal (⌃`)" aria-label="Terminal">
|
|
201
|
-
<svg viewBox="0 0 24 24"><rect x="2.5" y="4" width="19" height="16" rx="2.5"/><path d="M7 9.5l2.6 2.5L7 14.5"/><path d="M12.6 15h4.6"/></svg>
|
|
202
|
-
</button>
|
|
203
208
|
<span class="sep"></span>
|
|
204
209
|
<button type="button" class="sound" id="sound" aria-label="Sound on" title="Sound on"></button>
|
|
205
210
|
<button type="button" class="sound" id="speak" aria-label="Read replies aloud" title="Read replies aloud"></button>
|
|
@@ -209,24 +214,6 @@
|
|
|
209
214
|
<div class="banner" id="banner"></div>
|
|
210
215
|
<div class="toast" id="toast"></div>
|
|
211
216
|
<p class="hint">Drag to move · Right-drag or Q / E to turn · Scroll to zoom · Double-click to go there</p>
|
|
212
|
-
|
|
213
|
-
<section class="term" id="term" hidden aria-label="Terminal">
|
|
214
|
-
<div class="term-grip" id="termGrip" role="separator" aria-orientation="horizontal" aria-label="Resize the terminal" tabindex="0"></div>
|
|
215
|
-
<header class="term-bar">
|
|
216
|
-
<div class="term-tabs" id="termTabs" role="tablist" aria-label="Terminal sessions"></div>
|
|
217
|
-
<select id="termFolder" aria-label="Folder for the next terminal"></select>
|
|
218
|
-
<button type="button" class="term-icon" id="termNew" title="New terminal" aria-label="New terminal">
|
|
219
|
-
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>
|
|
220
|
-
</button>
|
|
221
|
-
<span class="term-fill"></span>
|
|
222
|
-
<span class="term-note" id="termNote"></span>
|
|
223
|
-
<button type="button" class="term-icon" id="termHide" title="Hide the terminal (⌃`)" aria-label="Hide the terminal">
|
|
224
|
-
<svg viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12"/></svg>
|
|
225
|
-
</button>
|
|
226
|
-
</header>
|
|
227
|
-
<div class="term-body" id="termBody"></div>
|
|
228
|
-
<div class="term-off" id="termOff" hidden></div>
|
|
229
|
-
</section>
|
|
230
217
|
</main>
|
|
231
218
|
|
|
232
219
|
<aside class="detail" id="detail" hidden></aside>
|
package/docs/README.md
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
- [Claude access](claude-access.md) — Claude Code login or API key, and the plan meters
|
|
18
18
|
- [Configuration](configuration.md) — every setting
|
|
19
19
|
- [Talking out loud](voice.md) — speech in and voices out, all on this machine
|
|
20
|
-
- [The terminal](terminal.md) — a real shell under the office
|
|
21
20
|
- [On your phone](phone.md) — install it like an app, reach it over Tailscale
|
|
22
21
|
|
|
23
22
|
**Under the hood**
|
package/docs/claude-access.md
CHANGED
|
@@ -13,8 +13,7 @@ The key button beside Appearance chooses how the office signs in to Claude:
|
|
|
13
13
|
The choice applies to every `claude` the office starts: jobs, the chief,
|
|
14
14
|
standup and break-room talk. With the login chosen, a stray
|
|
15
15
|
`ANTHROPIC_API_KEY` in your shell is kept away from them, since `claude` would
|
|
16
|
-
otherwise prefer it. Jobs already running finish the way they started.
|
|
17
|
-
in-app terminal is your own shell and never gets the office's key.
|
|
16
|
+
otherwise prefer it. Jobs already running finish the way they started.
|
|
18
17
|
|
|
19
18
|
## Plan usage
|
|
20
19
|
|
package/docs/configuration.md
CHANGED
|
@@ -46,12 +46,8 @@ voice toggle, team profiles, theme — are made in the office and remembered.
|
|
|
46
46
|
| `PIPER_BIN` | `data/piper/venv/bin/python` | the voices live in `data/piper/voices` |
|
|
47
47
|
| `FFMPEG_BIN` | `ffmpeg` | |
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## Leaderboard
|
|
50
50
|
|
|
51
51
|
| Env | Default | |
|
|
52
52
|
|---|---|---|
|
|
53
53
|
| `OFFICE_LEADERBOARD_URL` | the hosted board | address of a different leaderboard service; see [The leaderboard](leaderboard.md) |
|
|
54
|
-
| `OFFICE_TERMINAL` | unset | `1` opens the in-app terminal |
|
|
55
|
-
| `OFFICE_TERMINAL_SHELL` | your `$SHELL` | |
|
|
56
|
-
| `OFFICE_TERMINAL_MAX` | `4` | sessions at once |
|
|
57
|
-
| `OFFICE_TERMINAL_IDLE_MIN` | `30` | `0` never closes an idle shell |
|
package/docs/getting-started.md
CHANGED
|
@@ -44,7 +44,6 @@ the chat with `#`, e.g. `#storefront`.
|
|
|
44
44
|
| Extra | What it adds | How |
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| Voice | talk to the office, hear it answer | [Talking out loud](voice.md) |
|
|
47
|
-
| Terminal | a real shell in a drawer under the office | [The terminal](terminal.md) |
|
|
48
47
|
| Phone | the office on your phone, installed like an app | [On your phone](phone.md) |
|
|
49
48
|
|
|
50
49
|
## Next
|
|
@@ -86,16 +86,6 @@ browser's built-in voices.
|
|
|
86
86
|
Browsers only allow it on `localhost` or HTTPS. Open the office at
|
|
87
87
|
`http://localhost:4317`, not an IP address.
|
|
88
88
|
|
|
89
|
-
## The terminal won't open
|
|
90
|
-
|
|
91
|
-
**There's no terminal button, or it says the terminal is off.**
|
|
92
|
-
It's off by default. Start the office with `OFFICE_TERMINAL=1 npm start`.
|
|
93
|
-
|
|
94
|
-
**"node-pty is not installed."**
|
|
95
|
-
It's a native module and needs build tools. On macOS:
|
|
96
|
-
`xcode-select --install`, then `npm install`. On Linux: install
|
|
97
|
-
`build-essential` and `python3`, then `npm install`.
|
|
98
|
-
|
|
99
89
|
## On a phone
|
|
100
90
|
|
|
101
91
|
**"Local only." (HTTP 403)**
|
package/docs/guides/updating.md
CHANGED
|
@@ -38,5 +38,3 @@ Then restart it with `npm start`. To stay on a release, check out its tag
|
|
|
38
38
|
- **Keep a way back:** copy the data folder before updating. Going back is
|
|
39
39
|
then: stop the office, install the previous version, restore the folder,
|
|
40
40
|
start.
|
|
41
|
-
- **The terminal stops working after an update:** node-pty needs rebuilding;
|
|
42
|
-
see [Troubleshooting](troubleshooting.md#the-terminal-wont-open).
|
package/docs/how-it-works.md
CHANGED
|
@@ -52,7 +52,6 @@ If the server restarts mid-job, jobs with a session resume where they left off.
|
|
|
52
52
|
| `data/auth.json` | how Claude is signed in to, and the API key if you gave one (mode `0600`) |
|
|
53
53
|
| `data/uploads/` | attachments and voice notes |
|
|
54
54
|
| `data/whisper/`, `data/piper/` | speech models, if you installed them |
|
|
55
|
-
| `data/terminal.log` | when terminal sessions opened and closed |
|
|
56
55
|
|
|
57
56
|
`data/` is git-ignored. State is written shortly after every change, and the server stops its jobs cleanly on Ctrl-C so they can resume.
|
|
58
57
|
|
|
@@ -70,7 +69,6 @@ If the server restarts mid-job, jobs with a session resume where they left off.
|
|
|
70
69
|
| `leaderboard/` | the leaderboard service (a Cloudflare Worker) |
|
|
71
70
|
| `server/changes.js` | the "what changed" snapshots and diffs |
|
|
72
71
|
| `server/voice.js` | whisper.cpp transcription and Piper voices |
|
|
73
|
-
| `server/terminal.js` | the in-app terminal |
|
|
74
72
|
| `server/procstat.js` | agents' CPU and memory |
|
|
75
73
|
| `web/src/main.js` | browser entry: socket, state, wiring |
|
|
76
74
|
| `web/src/office.js`, `figure.js`, `agent.js` | the 3D office and the people in it |
|
package/docs/linked-offices.md
CHANGED
|
@@ -47,7 +47,7 @@ account. Pick **Invite a guest** and choose:
|
|
|
47
47
|
|
|
48
48
|
A guest can start up to three jobs at a time, reply to them, stop them, and see
|
|
49
49
|
their logs and changes. They can't see your other jobs, chat or channels, and
|
|
50
|
-
they can't
|
|
50
|
+
they can't restart the office, or change staff or settings.
|
|
51
51
|
Their jobs show `from @handle` in your job list, and you can stop any of them.
|
|
52
52
|
Agents working for a guest can't message or hand work to your other agents.
|
|
53
53
|
They also don't get your MCP servers or claude.ai connectors (mail, Slack,
|
package/docs/packaging.md
CHANGED
|
@@ -16,7 +16,7 @@ the package.
|
|
|
16
16
|
|---|---|---|---|
|
|
17
17
|
| **npm CLI** (`npx agent-office`) | Best: the audience already has Node and Claude Code | ~1 week | data and project root must move out of the package |
|
|
18
18
|
| **Homebrew tap** | Good extra for Mac users | ~1 day on top of npm | another channel to release to |
|
|
19
|
-
| **Desktop app** (Electron) | Friendlier, but still needs the `claude` CLI | 3–5 weeks | signing and notarisation, auto-update
|
|
19
|
+
| **Desktop app** (Electron) | Friendlier, but still needs the `claude` CLI | 3–5 weeks | signing and notarisation, auto-update |
|
|
20
20
|
| **Docker** | Poor for a local tool | ~1 week | agents edit *host* repos: bind mounts, file ownership, git/SSH keys, login inside the container |
|
|
21
21
|
| **Hosted** | A different product | months | accounts, a sandbox per user, repos via GitHub, a database, API billing; the local-only security model goes |
|
|
22
22
|
|
|
@@ -34,14 +34,13 @@ if users ask for one; leave hosted until the local version has users.
|
|
|
34
34
|
`OFFICE_DATA` override, migrating an existing `<app>/data` on first run.
|
|
35
35
|
- [ ] **Project root:** default to the current directory, `--root <dir>`,
|
|
36
36
|
remembered after the first run.
|
|
37
|
-
- [ ] **`bin/agent-office.js`:** `--port`, `--root`, `--open
|
|
37
|
+
- [ ] **`bin/agent-office.js`:** `--port`, `--root`, `--open`;
|
|
38
38
|
pick another port if 4317 is taken; open the browser.
|
|
39
39
|
- [ ] **Preflight:** `claude` on `PATH`, recent enough, and signed in or given a
|
|
40
40
|
key — one clear fix for each problem.
|
|
41
41
|
- [ ] **Package:** publish a prebuilt `dist/`; move the web-only dependencies
|
|
42
|
-
(`three`, `marked`,
|
|
42
|
+
(`three`, `marked`, fonts) to devDependencies so the runtime
|
|
43
43
|
needs only `ws`.
|
|
44
|
-
- [ ] **Terminal:** a `node-pty` build with prebuilt binaries, still optional.
|
|
45
44
|
- [ ] **Voice:** `agent-office voice setup` in place of the two npm scripts.
|
|
46
45
|
- [ ] **Plan meters:** hide quietly if the usage endpoint goes away.
|
|
47
46
|
- [ ] **Platforms:** CI on clean macOS and Ubuntu; Windows via WSL.
|
package/docs/phone.md
CHANGED
|
@@ -27,6 +27,5 @@ On the phone, open the tailnet address once with the office key on the end,
|
|
|
27
27
|
printed, or `agentoffice --link`). The phone keeps it after that.
|
|
28
28
|
|
|
29
29
|
**Read this first.** Whoever can open that address can run Claude in your
|
|
30
|
-
projects with full autonomy
|
|
31
|
-
this machine. Only expose it on a tailnet that is just you. Never put it on
|
|
30
|
+
projects with full autonomy. Only expose it on a tailnet that is just you. Never put it on
|
|
32
31
|
the open internet. See [Security](security.md).
|
package/docs/security.md
CHANGED
|
@@ -64,7 +64,7 @@ enforced on your side and the guest's office is never trusted:
|
|
|
64
64
|
- only the projects you picked;
|
|
65
65
|
- at or below the clearance cap you set;
|
|
66
66
|
- within the tokens per day you set;
|
|
67
|
-
- no
|
|
67
|
+
- no settings, chat, or other people's jobs;
|
|
68
68
|
- their agents can't reach your other agents;
|
|
69
69
|
- their jobs don't get your MCP servers or claude.ai connectors, your
|
|
70
70
|
user-level Claude Code settings and hooks, or your Claude Code memory.
|
|
@@ -72,9 +72,3 @@ enforced on your side and the guest's office is never trusted:
|
|
|
72
72
|
Even *Edit files* can change scripts or hooks that run later, so invite only
|
|
73
73
|
people you would trust with those projects. Details in
|
|
74
74
|
[Linked offices](linked-offices.md).
|
|
75
|
-
|
|
76
|
-
## The terminal
|
|
77
|
-
|
|
78
|
-
Off unless started with `--terminal` (`OFFICE_TERMINAL=1`). A session needs a single-use ticket from the
|
|
79
|
-
origin-checked API, no agent can open one, and keystrokes are never logged. It
|
|
80
|
-
is not a sandbox. Details in [The terminal](terminal.md).
|
package/docs/using-the-office.md
CHANGED
|
@@ -57,7 +57,7 @@ Drag to move, right-drag (or `Q` / `E`) to turn around what's in the middle
|
|
|
57
57
|
of the view, scroll to zoom toward the cursor, double-click to glide somewhere.
|
|
58
58
|
`W A S D` pan, `R` recenters. The walls nearest you drop down so you can always
|
|
59
59
|
see in. Chat and job details sit in their own columns and never cover the
|
|
60
|
-
office
|
|
60
|
+
office.
|
|
61
61
|
|
|
62
62
|
## Daily standup
|
|
63
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-office-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A 3D office where the staff are Claude Code agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -54,14 +54,9 @@
|
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@fontsource-variable/inter": "5.3.0",
|
|
56
56
|
"@fontsource/jetbrains-mono": "5.3.0",
|
|
57
|
-
"@xterm/addon-fit": "0.11.0",
|
|
58
|
-
"@xterm/xterm": "6.0.0",
|
|
59
57
|
"concurrently": "10.0.6",
|
|
60
58
|
"marked": "^18.1.0",
|
|
61
59
|
"three": "0.186.1",
|
|
62
60
|
"vite": "6.4.3"
|
|
63
|
-
},
|
|
64
|
-
"optionalDependencies": {
|
|
65
|
-
"node-pty": "1.0.0"
|
|
66
61
|
}
|
|
67
62
|
}
|
package/server/icons.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Finds a project's own app icon or favicon so the office can show it beside
|
|
5
|
+
// the project's name. Looks in the usual places, then a shallow scan.
|
|
6
|
+
|
|
7
|
+
const TYPES = { '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp' };
|
|
8
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.expo', 'Pods', 'vendor', 'coverage', 'out', 'target', 'data', 'DerivedData']);
|
|
9
|
+
const NAME = /^(favicon|app-?icon|apple-touch-icon|icon|logo|ic_launcher)([-_.@x\d\w]*)\.(svg|png|ico|jpe?g|webp)$/i;
|
|
10
|
+
const MAX_BYTES = 1024 * 1024;
|
|
11
|
+
const MAX_DEPTH = 4;
|
|
12
|
+
const MAX_VISITS = 1500;
|
|
13
|
+
|
|
14
|
+
const score = (rel, size) => {
|
|
15
|
+
const parts = rel.split(path.sep);
|
|
16
|
+
const base = parts[parts.length - 1].toLowerCase();
|
|
17
|
+
const ext = path.extname(base);
|
|
18
|
+
let s = 0;
|
|
19
|
+
if (/^(app-?icon|icon)\./.test(base)) s += 40;
|
|
20
|
+
else if (/^apple-touch-icon/.test(base)) s += 36;
|
|
21
|
+
else if (/^favicon/.test(base)) s += 30;
|
|
22
|
+
else if (/^logo/.test(base)) s += 20;
|
|
23
|
+
else s += 10;
|
|
24
|
+
if (ext === '.svg') s += 12; else if (ext === '.png') s += 8; else if (ext === '.ico') s -= 4;
|
|
25
|
+
if (parts.some((p) => /^(public|assets|static|resources|app|icons?|images|web)$/i.test(p))) s += 6;
|
|
26
|
+
if (parts.some((p) => /^(test|tests|__tests__|docs?|examples?|fixtures?|storybook|screenshots?)$/i.test(p))) s -= 25;
|
|
27
|
+
s -= (parts.length - 1) * 3;
|
|
28
|
+
s += Math.min(size / 20000, 4); // among equals, the sharper one
|
|
29
|
+
return s;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function scan(root) {
|
|
33
|
+
const found = [];
|
|
34
|
+
let visits = 0;
|
|
35
|
+
const walk = (dir, depth) => {
|
|
36
|
+
let entries;
|
|
37
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (++visits > MAX_VISITS) return;
|
|
40
|
+
const full = path.join(dir, e.name);
|
|
41
|
+
if (e.isDirectory()) {
|
|
42
|
+
if (!SKIP.has(e.name) && !e.name.startsWith('.') && depth < MAX_DEPTH) {
|
|
43
|
+
// iOS keeps its icons in an appiconset; the biggest PNG is the one to use.
|
|
44
|
+
if (e.name === 'AppIcon.appiconset') {
|
|
45
|
+
try {
|
|
46
|
+
const pngs = fs.readdirSync(full).filter((f) => /\.png$/i.test(f)).map((f) => [f, fs.statSync(path.join(full, f)).size]).sort((a, b) => b[1] - a[1]);
|
|
47
|
+
if (pngs[0] && pngs[0][1] <= MAX_BYTES) found.push({ full: path.join(full, pngs[0][0]), score: 38 - depth * 3 });
|
|
48
|
+
} catch {}
|
|
49
|
+
} else walk(full, depth + 1);
|
|
50
|
+
}
|
|
51
|
+
} else if (e.isFile() && NAME.test(e.name)) {
|
|
52
|
+
try {
|
|
53
|
+
const size = fs.statSync(full).size;
|
|
54
|
+
if (size > 0 && size <= MAX_BYTES) found.push({ full, score: score(path.relative(root, full), size) });
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(root, 0);
|
|
60
|
+
return found.sort((a, b) => b.score - a.score)[0]?.full || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cache = new Map(); // repo dir -> { at, file }
|
|
64
|
+
const TTL = 5 * 60 * 1000;
|
|
65
|
+
|
|
66
|
+
// Returns { file, type } for a project folder, or null when it has no icon.
|
|
67
|
+
export function findIcon(dir) {
|
|
68
|
+
const hit = cache.get(dir);
|
|
69
|
+
if (hit && Date.now() - hit.at < TTL) return hit.file;
|
|
70
|
+
let file = null;
|
|
71
|
+
try {
|
|
72
|
+
const found = scan(dir);
|
|
73
|
+
const real = found && fs.realpathSync(found);
|
|
74
|
+
// A symlink must not lead out of the project.
|
|
75
|
+
if (real && real.startsWith(fs.realpathSync(dir) + path.sep)) {
|
|
76
|
+
const type = TYPES[path.extname(real).toLowerCase()];
|
|
77
|
+
if (type) file = { file: real, type };
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
cache.set(dir, { at: Date.now(), file });
|
|
81
|
+
return file;
|
|
82
|
+
}
|
package/server/index.js
CHANGED
|
@@ -13,13 +13,13 @@ import { writeStandup, dueToday } from './standup.js';
|
|
|
13
13
|
import { writeBanter } from './banter.js';
|
|
14
14
|
import { sampleProcs, resetProcs, machine } from './procstat.js';
|
|
15
15
|
import { transcribe, vocabulary, voiceReady, speak, piperReady } from './voice.js';
|
|
16
|
-
import { Terminals } from './terminal.js';
|
|
17
16
|
import { snapshotTree, summarize, fileDiff } from './changes.js';
|
|
18
17
|
import { Auth } from './auth.js';
|
|
19
18
|
import { Leaderboard } from './leaderboard.js';
|
|
20
19
|
import { Links } from './links.js';
|
|
21
20
|
import { Settings } from './settings.js';
|
|
22
21
|
import { Guests } from './guests.js';
|
|
22
|
+
import { findIcon } from './icons.js';
|
|
23
23
|
|
|
24
24
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
25
25
|
const APP = path.resolve(HERE, '..');
|
|
@@ -1973,10 +1973,6 @@ function serveUpload(res, id, name) {
|
|
|
1973
1973
|
|
|
1974
1974
|
// ---------- http ----------
|
|
1975
1975
|
|
|
1976
|
-
// The in-app terminal. Off unless OFFICE_TERMINAL=1; see server/terminal.js
|
|
1977
|
-
// for why its door is built the way it is.
|
|
1978
|
-
const terminals = new Terminals({ root: ROOT, dataDir: DATA, folders: listRepos });
|
|
1979
|
-
|
|
1980
1976
|
// OFFICE_ALLOWED_HOSTS adds exact hostnames (comma-separated), e.g. this
|
|
1981
1977
|
// machine's Tailscale name when `tailscale serve` fronts the office. Everything
|
|
1982
1978
|
// else is still refused, which is what keeps rebinding sites out.
|
|
@@ -2113,14 +2109,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
2113
2109
|
if (parts[1] !== 'agent' && !hasKey(req)) return json(res, 401, { error: 'Open the office with its link first.' });
|
|
2114
2110
|
|
|
2115
2111
|
try {
|
|
2116
|
-
if (req.method === 'GET' && url.pathname === '/api/repos')
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2112
|
+
if (req.method === 'GET' && url.pathname === '/api/repos/icon') {
|
|
2113
|
+
const dir = repoPath(url.searchParams.get('name'));
|
|
2114
|
+
const icon = dir && findIcon(dir);
|
|
2115
|
+
if (!icon) return json(res, 404, { error: 'No icon.' });
|
|
2116
|
+
res.writeHead(200, {
|
|
2117
|
+
'content-type': icon.type, 'cache-control': 'private, max-age=300',
|
|
2118
|
+
'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'; sandbox", 'x-content-type-options': 'nosniff',
|
|
2119
|
+
});
|
|
2120
|
+
return fs.createReadStream(icon.file).pipe(res);
|
|
2123
2121
|
}
|
|
2122
|
+
if (req.method === 'GET' && url.pathname === '/api/repos') return json(res, 200, { root: ROOT, repos: listRepos() });
|
|
2124
2123
|
if (req.method === 'POST' && parts[1] === 'agent' && agentApi[parts[2]]) {
|
|
2125
2124
|
const me = caller(req);
|
|
2126
2125
|
if (!me) return json(res, 403, { error: 'Not on the clock.' });
|
|
@@ -2308,9 +2307,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2308
2307
|
});
|
|
2309
2308
|
|
|
2310
2309
|
const wss = new WebSocketServer({ noServer: true });
|
|
2311
|
-
// A keystroke at a time, so nothing here needs a big frame; a terminal that
|
|
2312
|
-
// could be fed a megabyte per message is just a memory hole.
|
|
2313
|
-
const termWss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
|
|
2314
2310
|
server.on('upgrade', (req, socket, head) => {
|
|
2315
2311
|
const url = new URL(req.url, 'http://x');
|
|
2316
2312
|
if (!trusted(req)) return socket.destroy();
|
|
@@ -2330,11 +2326,6 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
2330
2326
|
...snapshot(), dismissed: [], standup: null, chiefBusy: false, procs: null, visitors: [],
|
|
2331
2327
|
})));
|
|
2332
2328
|
}
|
|
2333
|
-
// No terminal, no socket: with the door shut there is nothing to hold open
|
|
2334
|
-
// while someone guesses at tickets.
|
|
2335
|
-
if (url.pathname === '/ws/terminal' && terminals.on) {
|
|
2336
|
-
return termWss.handleUpgrade(req, socket, head, (ws) => terminals.open(ws, url.searchParams.get('ticket')));
|
|
2337
|
-
}
|
|
2338
2329
|
socket.destroy();
|
|
2339
2330
|
});
|
|
2340
2331
|
|
|
@@ -2351,10 +2342,6 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
2351
2342
|
console.log(`Agent Office open on ${keyLink()} (projects in ${ROOT})`);
|
|
2352
2343
|
// For `agentoffice --link`, which can't know which free port we got.
|
|
2353
2344
|
try { fs.writeFileSync(path.join(DATA, 'office-port'), String(PORT)); } catch {}
|
|
2354
|
-
// Shell access being open is not something to find out by accident.
|
|
2355
|
-
terminals.status().then((t) => console.log(t.enabled
|
|
2356
|
-
? `Terminal ON — anything in this browser gets a shell as ${process.env.USER || 'you'}. Up to ${t.max}, ${t.idleMinutes} min idle.`
|
|
2357
|
-
: `Terminal off. ${t.reason}`));
|
|
2358
2345
|
afterRestart();
|
|
2359
2346
|
pump();
|
|
2360
2347
|
refreshUsage();
|
|
@@ -2364,7 +2351,6 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
2364
2351
|
|
|
2365
2352
|
function shutdown(code = 0) {
|
|
2366
2353
|
for (const child of running.values()) child.kill('SIGTERM');
|
|
2367
|
-
terminals.killAll();
|
|
2368
2354
|
clearTimeout(saveTimer);
|
|
2369
2355
|
board.flush();
|
|
2370
2356
|
fs.mkdirSync(DATA, { recursive: true });
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
function e(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}export{e as g};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{g as f}from"./_commonjsHelpers-CqkleIqs.js";function g(i,n){for(var o=0;o<n.length;o++){const t=n[o];if(typeof t!="string"&&!Array.isArray(t)){for(const e in t)if(e!=="default"&&!(e in i)){const r=Object.getOwnPropertyDescriptor(t,e);r&&Object.defineProperty(i,e,r.get?r:{enumerable:!0,get:()=>t[e]})}}}return Object.freeze(Object.defineProperty(i,Symbol.toStringTag,{value:"Module"}))}var l={exports:{}};(function(i,n){(function(o,t){i.exports=t()})(self,()=>(()=>{var o={};return(()=>{var t=o;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const r=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(r._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal||!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,r=e._renderService.dimensions;if(r.css.cell.width===0||r.css.cell.height===0)return;const c=this._terminal.options.scrollback===0?0:e.viewport.scrollBarWidth,a=window.getComputedStyle(this._terminal.element.parentElement),d=parseInt(a.getPropertyValue("height")),m=Math.max(0,parseInt(a.getPropertyValue("width"))),s=window.getComputedStyle(this._terminal.element),h=d-(parseInt(s.getPropertyValue("padding-top"))+parseInt(s.getPropertyValue("padding-bottom"))),u=m-(parseInt(s.getPropertyValue("padding-right"))+parseInt(s.getPropertyValue("padding-left")))-c;return{cols:Math.max(2,Math.floor(u/r.css.cell.width)),rows:Math.max(1,Math.floor(h/r.css.cell.height))}}}})(),o})())})(l);var p=l.exports;const _=f(p),w=g({__proto__:null,default:_},[p]);export{w as a};
|