@caglarutkuguler/cubicle 0.3.0 → 0.4.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/README.md +37 -10
- package/bin/cubicle.js +5 -0
- package/package.json +1 -1
- package/public/index.html +44 -6
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
**[▶ Live demo](https://caglarutkuguler.github.io/cubicle/?lang=en)** — no install, fake agents.
|
|
6
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/@caglarutkuguler/cubicle) [](https://github.com/caglarutkuguler/cubicle/actions/workflows/ci.yml)  [](LICENSE)
|
|
8
|
+
|
|
7
9
|
Your agents get a desk. When one starts working it walks over, sits down and starts typing, with its current task floating above its head. When it needs you, it raises a hand. When it finishes it goes back to the lounge for a coffee. If it hits an error, its screen flashes red.
|
|
8
10
|
|
|
9
11
|
[](https://caglarutkuguler.github.io/cubicle/?lang=en)
|
|
@@ -21,21 +23,21 @@ Requires Node.js 18+.
|
|
|
21
23
|
**Paperclip** (default; expects Paperclip at `http://127.0.0.1:3100`):
|
|
22
24
|
|
|
23
25
|
```bash
|
|
24
|
-
npx
|
|
26
|
+
npx @caglarutkuguler/cubicle
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
**Claude Code** (one character per session, driven by Claude Code's own hooks; details in [`examples/claude-code/`](examples/claude-code/)):
|
|
28
30
|
|
|
29
31
|
```bash
|
|
30
|
-
npx
|
|
31
|
-
npx
|
|
32
|
+
npx @caglarutkuguler/cubicle install-hooks # once; backs up ~/.claude/settings.json first
|
|
33
|
+
npx @caglarutkuguler/cubicle --source claude-code
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
**Anything else** — a JSON file or URL in the [feed format](docs/FEED.md):
|
|
35
37
|
|
|
36
38
|
```bash
|
|
37
|
-
npx
|
|
38
|
-
npx
|
|
39
|
+
npx @caglarutkuguler/cubicle --source ./agents.json
|
|
40
|
+
npx @caglarutkuguler/cubicle --source http://localhost:8080/agents
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
Then open <http://127.0.0.1:3200>. Nothing running yet? Open <http://127.0.0.1:3200/?demo>, or the [hosted demo](https://caglarutkuguler.github.io/cubicle/?lang=en).
|
|
@@ -63,13 +65,14 @@ URL parameters:
|
|
|
63
65
|
- `?company=PREFIX` opens a specific Paperclip company (for example `?company=MEG`). With several companies, a picker also appears in the header.
|
|
64
66
|
- `?lang=en` or `?lang=tr` sets the language.
|
|
65
67
|
- `?demo` shows fake agents.
|
|
68
|
+
- `?kiosk` fills the screen with the office for a TV or a second monitor: cards and footer hidden, names and bubbles scale with the screen, cursor hidden. Combined with the auto-updater, a wall display picks up new versions by itself.
|
|
66
69
|
|
|
67
70
|
## What the office shows
|
|
68
71
|
|
|
69
72
|
| Status | In the office |
|
|
70
73
|
| --- | --- |
|
|
71
74
|
| `running` | Sits at its desk, monitor on, typing; bubble shows the task |
|
|
72
|
-
| `waiting` | Sits at its desk with a hand up, amber screen — it needs your input or approval |
|
|
75
|
+
| `waiting` | Sits at its desk with a hand up, amber screen — it needs your input or approval. With Paperclip this comes from issues whose review is waiting on the board (for example pending ask-user questions); a busy agent keeps typing but its bubble flashes the issue id, and the header counts how many need you |
|
|
73
76
|
| `idle` | Wanders the lounge; says "✓ done" right after finishing a run |
|
|
74
77
|
| `error` | Slumped at its desk, red screen, error text on its card |
|
|
75
78
|
| `paused` | "zZ" |
|
|
@@ -83,9 +86,9 @@ Each card below the office links to the agent's current task when the source pro
|
|
|
83
86
|
The default `local_trusted` mode needs no key. For an authenticated or remote Paperclip, give Cubicle an API key; it is sent as `Authorization: Bearer …` on the forwarded requests only:
|
|
84
87
|
|
|
85
88
|
```bash
|
|
86
|
-
PAPERCLIP_URL=https://paperclip.example.com PAPERCLIP_TOKEN=pcp_… npx
|
|
89
|
+
PAPERCLIP_URL=https://paperclip.example.com PAPERCLIP_TOKEN=pcp_… npx @caglarutkuguler/cubicle
|
|
87
90
|
# or keep it in a file only you can read
|
|
88
|
-
npx
|
|
91
|
+
npx @caglarutkuguler/cubicle --paperclip https://paperclip.example.com --token-file ~/.config/cubicle/paperclip-token
|
|
89
92
|
```
|
|
90
93
|
|
|
91
94
|
Use a board API key with the narrowest read-only scope Paperclip lets you create; an agent key also works but only sees that agent's company. There is no `--token` flag on purpose, because command-line flags are visible in the process list. With systemd, put `PAPERCLIP_TOKEN=…` in a `chmod 600` file and point `EnvironmentFile=` at it (see `examples/systemd/cubicle.service`).
|
|
@@ -94,6 +97,21 @@ Use a board API key with the narrowest read-only scope Paperclip lets you create
|
|
|
94
97
|
|
|
95
98
|
**Feed.** Any process can write `{ "company": "…", "agents": [{ "id", "name", "role", "status", "task", "error" }] }` to a file or serve it over HTTP. Full spec with status aliases: [`docs/FEED.md`](docs/FEED.md). A sample is in [`examples/feed.json`](examples/feed.json).
|
|
96
99
|
|
|
100
|
+
## How it compares
|
|
101
|
+
|
|
102
|
+
Cubicle is one of several pixel offices for AI agents, all inspired by [Pixel Agents](https://github.com/pixel-agents-hq/pixel-agents). They optimise for different things:
|
|
103
|
+
|
|
104
|
+
| | Cubicle | [Pixel Agents](https://github.com/pixel-agents-hq/pixel-agents) | [Agent Pixels](https://github.com/gcampton/Agent-Pixels) | [agents-in-the-office](https://github.com/gukosowa/agents-in-the-office) |
|
|
105
|
+
| --- | --- | --- | --- | --- |
|
|
106
|
+
| Works with | Paperclip, Claude Code, any JSON feed | Claude Code | Paperclip | Claude Code, Gemini CLI |
|
|
107
|
+
| Runs as | Standalone page (`npx`, systemd, kiosk) | VS Code extension or `npx` browser app | Paperclip plugin | Standalone app |
|
|
108
|
+
| Setup | One command, nothing to build | Marketplace install; build from source to hack on it | Build against the Paperclip plugin SDK | See its README |
|
|
109
|
+
| Access to your agents | Read-only by design (GET allowlist) | Watches local Claude Code sessions | Inside Paperclip | Watches local sessions |
|
|
110
|
+
| Shows "needs you" | Yes: permission prompts, board questions | Yes: waiting for approval | Not listed | Yes: approval alerts |
|
|
111
|
+
| Office editor, art | Fixed room, programmer art | Layout editor, furniture and character assets, pets | 80+ characters, multi-room views | Tile map editor, sound packs |
|
|
112
|
+
|
|
113
|
+
**Pick Cubicle** if you want something you can trust to run next to production agents (it can only read), that starts in seconds with no build, or that shows Paperclip and Claude Code in one place. **Pick another one** if you want to design your office, richer art, or tighter editor integration.
|
|
114
|
+
|
|
97
115
|
## Run it as a service (Linux / WSL)
|
|
98
116
|
|
|
99
117
|
systemd user units are in [`examples/systemd/`](examples/systemd/): `cubicle.service` (Paperclip, port 3200), `cubicle-claude.service` (Claude Code, port 3201), and an optional updater.
|
|
@@ -129,7 +147,16 @@ journalctl --user -u cubicle-update.service # what it did
|
|
|
129
147
|
|
|
130
148
|
## Contributing
|
|
131
149
|
|
|
132
|
-
Issues and pull requests are welcome. Ideas: adapters for other agent runtimes (write a feed, send a PR with an example), a kiosk mode for wall displays, meeting-room animations when agents hand work to each other, per-agent sprite customisation, replay of a recorded day, and multi-source offices (Paperclip and Claude Code on one floor).
|
|
150
|
+
Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md) for the few ground rules (zero dependencies, read-only). Look for issues labelled [good first issue](https://github.com/caglarutkuguler/cubicle/labels/good%20first%20issue). Ideas: adapters for other agent runtimes (write a feed, send a PR with an example), a kiosk mode for wall displays, meeting-room animations when agents hand work to each other, per-agent sprite customisation, replay of a recorded day, and multi-source offices (Paperclip and Claude Code on one floor).
|
|
151
|
+
|
|
152
|
+
## Releasing
|
|
153
|
+
|
|
154
|
+
Versions are published to npm by [`.github/workflows/release.yml`](.github/workflows/release.yml) with npm trusted publishing, so no npm token exists anywhere and every version carries a provenance attestation.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm version patch # or minor / major: bumps package.json, commits, tags vX.Y.Z
|
|
158
|
+
git push --follow-tags # CI tests, publishes to npm, creates the GitHub release
|
|
159
|
+
```
|
|
133
160
|
|
|
134
161
|
## Credits
|
|
135
162
|
|
|
@@ -147,4 +174,4 @@ Cubicle is a community project and is not affiliated with or endorsed by Papercl
|
|
|
147
174
|
|
|
148
175
|
**Cubicle**, AI ajanlarınızı canlı bir pixel ofiste gösterir: [Paperclip](https://github.com/paperclipai/paperclip) ve Claude Code hazır gelir; başka sistemler küçük bir [JSON feed](docs/FEED.md) ile bağlanır. Çalışan ajan masasına oturup yazar ve başının üstünde görevi görünür; sizi bekleyen ajan elini kaldırır; işi biten ajan dinlenme alanına döner; hata alan ajanın ekranı kırmızı yanar.
|
|
149
176
|
|
|
150
|
-
Kurulum: `npx
|
|
177
|
+
Kurulum: `npx @caglarutkuguler/cubicle` (Paperclip) veya `npx @caglarutkuguler/cubicle --source claude-code` (Claude Code) çalıştırın ve <http://127.0.0.1:3200> adresini açın. Kurmadan denemek için: [canlı demo](https://caglarutkuguler.github.io/cubicle/?lang=tr). Arayüz tarayıcı diline göre Türkçe açılır; `?lang=tr` ile de seçilebilir.
|
package/bin/cubicle.js
CHANGED
|
@@ -32,6 +32,11 @@ if (process.argv[2] === 'install-hooks') {
|
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
36
|
+
console.log(require('../package.json').version);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
36
41
|
console.log(`Cubicle — a live pixel-art office for your AI agents
|
|
37
42
|
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -17,13 +17,21 @@
|
|
|
17
17
|
select { background:var(--panel); color:var(--text); border:1px solid var(--line); border-radius:6px; padding:3px 6px; font:inherit; }
|
|
18
18
|
#stage { position:relative; width:100%; aspect-ratio:352/208; border:2px solid var(--line); border-radius:8px; overflow:hidden; background:#2a2d3d; }
|
|
19
19
|
canvas { position:absolute; inset:0; width:100%; height:100%; image-rendering:pixelated; }
|
|
20
|
-
#labels { position:absolute; inset:0; pointer-events:none; }
|
|
20
|
+
#labels { position:absolute; inset:0; pointer-events:none; font-size:11px; }
|
|
21
21
|
.lbl { position:absolute; transform:translate(-50%,-100%); display:flex; flex-direction:column; align-items:center; white-space:nowrap; }
|
|
22
|
-
.bub { background:#fff; color:#1d1f2b; border-radius
|
|
22
|
+
.bub { background:#fff; color:#1d1f2b; border-radius:.82em; padding:.18em .64em; font-size:1em; font-weight:600; margin-bottom:.27em; box-shadow:0 1px 0 rgba(0,0,0,.3); max-width:210px; overflow:hidden; text-overflow:ellipsis; }
|
|
23
23
|
.bub.err { background:#ffdede; color:#8a1111; }
|
|
24
24
|
.bub.ok { background:#dcffe2; color:#145c24; }
|
|
25
25
|
.bub.wait { background:#fff1c9; color:#6b4d00; }
|
|
26
|
-
.name { background:rgba(15,16,26,.78); color:#fff; font-size:
|
|
26
|
+
.name { background:rgba(15,16,26,.78); color:#fff; font-size:1em; font-weight:600; padding:.09em .55em; border-radius:.45em; }
|
|
27
|
+
/* ?kiosk: the office fills the screen, for a TV or a second monitor */
|
|
28
|
+
body.kiosk { overflow:hidden; cursor:none; }
|
|
29
|
+
body.kiosk .wrap { max-width:none; padding:0; height:100vh; display:flex; align-items:center; justify-content:center; }
|
|
30
|
+
body.kiosk header { position:fixed; top:0; left:0; right:0; z-index:2; margin:0; padding:1vh 1.4vw; background:linear-gradient(rgba(10,11,18,.8), rgba(10,11,18,0)); }
|
|
31
|
+
body.kiosk h1 { font-size:clamp(16px, 1.5vw, 34px); }
|
|
32
|
+
body.kiosk h1 small, body.kiosk #status { font-size:clamp(12px, 1.05vw, 24px); }
|
|
33
|
+
body.kiosk #stage { width:min(100vw, calc(100vh * 352 / 208)); border:0; border-radius:0; }
|
|
34
|
+
body.kiosk #cards, body.kiosk footer, body.kiosk #banner, body.kiosk #company { display:none !important; }
|
|
27
35
|
#banner { display:none; margin-bottom:10px; padding:8px 12px; border-radius:8px; background:#2b2f47; border:1px solid var(--line); color:var(--muted); }
|
|
28
36
|
#cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:8px; margin-top:12px; }
|
|
29
37
|
.card { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:9px 11px; }
|
|
@@ -75,6 +83,7 @@
|
|
|
75
83
|
noCompany: 'No company yet.', summary: (n, r) => `${n} agents · ${r} working`,
|
|
76
84
|
running: 'working', idle: 'idle', error: 'error', paused: 'paused', waiting: 'needs you', noTask: 'no open task assigned',
|
|
77
85
|
done: '✓ done', errBubble: '⚠ error', waitBubble: '✋ needs you',
|
|
86
|
+
asks: 'waiting for your answer on', needYou: (n) => `${n} need${n === 1 ? 's' : ''} you`,
|
|
78
87
|
chatter: ['☕', 'coffee break', '…', 'hmm', "what's next?"],
|
|
79
88
|
hint: 'Working agents sit at their desks and type; idle agents hang out in the lounge. Refreshes every 4 s.',
|
|
80
89
|
demo: 'Demo mode — fake agents. Remove <code>?demo</code> from the URL to watch your own Paperclip company.',
|
|
@@ -87,6 +96,7 @@
|
|
|
87
96
|
noCompany: 'Henüz şirket yok.', summary: (n, r) => `${n} ajan · ${r} çalışıyor`,
|
|
88
97
|
running: 'çalışıyor', idle: 'boşta', error: 'hata', paused: 'duraklatıldı', waiting: 'sizi bekliyor', noTask: 'atanmış açık görev yok',
|
|
89
98
|
done: '✓ bitirdi', errBubble: '⚠ hata', waitBubble: '✋ sizi bekliyor',
|
|
99
|
+
asks: 'cevabınızı bekliyor:', needYou: (n) => `${n} sizi bekliyor`,
|
|
90
100
|
chatter: ['☕', 'kahve molası', '…', 'hmm', 'sıradaki iş?'],
|
|
91
101
|
hint: 'Çalışan ajan masasına oturup yazar; boştaki ajan dinlenme alanında dolaşır. 4 saniyede bir yenilenir.',
|
|
92
102
|
demo: "Demo modu: sahte ajanlar. Kendi Paperclip şirketinizi izlemek için adresten <code>?demo</code> kısmını kaldırın.",
|
|
@@ -94,12 +104,16 @@
|
|
|
94
104
|
},
|
|
95
105
|
};
|
|
96
106
|
const params = new URLSearchParams(location.search);
|
|
97
|
-
|
|
107
|
+
// Any language with a block in STR is picked from ?lang=xx or the browser language; English otherwise.
|
|
108
|
+
const wantLang = (params.get('lang') || navigator.language || 'en').slice(0, 2).toLowerCase();
|
|
109
|
+
const lang = STR[wantLang] ? wantLang : 'en';
|
|
98
110
|
const L = STR[lang];
|
|
99
111
|
document.documentElement.lang = lang;
|
|
100
112
|
// Becomes true in init() as well when the page is served without the Cubicle
|
|
101
113
|
// proxy behind it (e.g. the hosted demo on GitHub Pages).
|
|
102
114
|
let DEMO = params.has('demo');
|
|
115
|
+
const KIOSK = params.has('kiosk');
|
|
116
|
+
if (KIOSK) document.body.classList.add('kiosk');
|
|
103
117
|
|
|
104
118
|
// ---------- setup ----------
|
|
105
119
|
const T = 16, CW = 352, CH = 208;
|
|
@@ -244,7 +258,8 @@
|
|
|
244
258
|
else b.style.display = 'none';
|
|
245
259
|
}
|
|
246
260
|
const running = agents.filter((a) => a.status === 'running').length;
|
|
247
|
-
|
|
261
|
+
const needYou = [...sprites.values()].filter((x) => x.status === 'waiting' || x.ask).length;
|
|
262
|
+
statusEl.textContent = `${L.summary(agents.length, running)}${needYou ? ' · ' + L.needYou(needYou) : ''} · ${new Date().toLocaleTimeString(lang)}`;
|
|
248
263
|
} catch (e) {
|
|
249
264
|
const code = Number(e && e.message);
|
|
250
265
|
statusEl.textContent = src === feed ? L.feedUnreachable
|
|
@@ -270,6 +285,18 @@
|
|
|
270
285
|
return null;
|
|
271
286
|
}
|
|
272
287
|
|
|
288
|
+
// Paperclip: an open issue whose review is waiting on the board (a pending question or
|
|
289
|
+
// interaction for a human) means the agent needs you, even though Paperclip reports it idle.
|
|
290
|
+
function askingBoard(agentId, issues) {
|
|
291
|
+
for (const i of issues) {
|
|
292
|
+
if (i.assigneeAgentId !== agentId || ['done', 'cancelled'].includes(i.status)) continue;
|
|
293
|
+
const paths = (i.reviewAttention && i.reviewAttention.paths) || [];
|
|
294
|
+
const p = paths.find((x) => /board|user|human/i.test(String(x && x.responder || '')));
|
|
295
|
+
if (p) return { issue: i, reason: String(p.label || '') };
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
273
300
|
function sync(agents, issues) {
|
|
274
301
|
const sorted = [...agents].sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
|
275
302
|
const seen = new Set();
|
|
@@ -300,6 +327,9 @@
|
|
|
300
327
|
s.status = a.status;
|
|
301
328
|
s.error = a.errorReason || '';
|
|
302
329
|
s.task = a.task !== undefined ? a.task : currentTask(a.id, issues);
|
|
330
|
+
s.waitReason = '';
|
|
331
|
+
s.ask = a.task === undefined && a.status !== 'error' ? askingBoard(a.id, issues) : null;
|
|
332
|
+
if (s.ask && a.status === 'idle') { s.status = 'waiting'; s.task = s.ask.issue; s.waitReason = s.ask.reason; }
|
|
303
333
|
if (s.prevStatus === 'running' && s.status === 'idle') setBubble(s, L.done, 'ok', 6);
|
|
304
334
|
s.prevStatus = s.status;
|
|
305
335
|
s.el.querySelector('.name').textContent = s.name;
|
|
@@ -324,7 +354,9 @@
|
|
|
324
354
|
cardsEl.innerHTML = list.map((s) => {
|
|
325
355
|
const t = s.task;
|
|
326
356
|
const taskHtml = t ? `${taskLink(t)} ${esc(t.title)}` : `<span style="color:var(--muted)">${L.noTask}</span>`;
|
|
327
|
-
const err = s.status === 'error' && s.error ? `<div class="st" style="color:#ffb3b8">${esc(s.error)}</div>`
|
|
357
|
+
const err = s.status === 'error' && s.error ? `<div class="st" style="color:#ffb3b8">${esc(s.error)}</div>`
|
|
358
|
+
: s.status === 'waiting' && s.waitReason ? `<div class="st" style="color:#ffe08a">${esc(s.waitReason)}</div>`
|
|
359
|
+
: s.ask ? `<div class="st" style="color:#ffe08a">✋ ${L.asks} ${taskLink(s.ask.issue)} ${esc(s.ask.issue.title)}</div>` : '';
|
|
328
360
|
return `<div class="card"><div class="top"><span class="sw" style="background:${s.shirt}"></span><b>${esc(s.name)}</b></div>
|
|
329
361
|
<div class="role">${esc(s.role)}</div>
|
|
330
362
|
<div class="st"><span class="pill p-${esc(s.status)}">${esc(L[s.status] || s.status)}</span>${taskHtml}</div>${err}</div>`;
|
|
@@ -472,14 +504,20 @@
|
|
|
472
504
|
|
|
473
505
|
function placeLabels(now) {
|
|
474
506
|
const k = cv.clientWidth / CW;
|
|
507
|
+
// Keep bubbles narrower than the gap between neighbouring desks so they never overlap.
|
|
508
|
+
const bubMax = Math.max(80, Math.floor(3.3 * T * k - 6)) + 'px';
|
|
509
|
+
// In kiosk mode names and bubbles grow with the screen (11px at the default ~3x scale).
|
|
510
|
+
if (KIOSK) labelsEl.style.fontSize = Math.max(11, Math.round(11 * k / 3)) + 'px';
|
|
475
511
|
for (const s of sprites.values()) {
|
|
476
512
|
s.el.style.left = (s.x * k) + 'px';
|
|
477
513
|
s.el.style.top = ((s.y - 20) * k) + 'px';
|
|
478
514
|
const b = s.el.querySelector('.bub');
|
|
515
|
+
b.style.maxWidth = bubMax;
|
|
479
516
|
let text = null, cls = '';
|
|
480
517
|
if (s.bubble && now < s.bubbleUntil) { text = s.bubble; cls = s.bubbleClass; }
|
|
481
518
|
else if (s.status === 'error' && s.seated) { text = L.errBubble; cls = 'err'; }
|
|
482
519
|
else if (s.status === 'waiting' && s.seated) { text = L.waitBubble; cls = 'wait'; }
|
|
520
|
+
else if (s.typing && s.ask && Math.floor(now / 3) % 2) { text = '✋ ' + (s.ask.issue.identifier || L.waiting); cls = 'wait'; }
|
|
483
521
|
else if (s.typing && s.task) text = '⌨ ' + (s.task.identifier || s.task.title);
|
|
484
522
|
else if (s.status === 'paused') text = 'zZ';
|
|
485
523
|
if (text) { b.textContent = text; b.className = 'bub ' + cls; b.style.display = ''; }
|