@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 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
+ [![npm](https://img.shields.io/npm/v/@caglarutkuguler/cubicle)](https://www.npmjs.com/package/@caglarutkuguler/cubicle) [![CI](https://github.com/caglarutkuguler/cubicle/actions/workflows/ci.yml/badge.svg)](https://github.com/caglarutkuguler/cubicle/actions/workflows/ci.yml) ![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen) [![MIT](https://img.shields.io/badge/license-MIT-blue)](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
  [![Cubicle demo](docs/demo.gif)](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 github:caglarutkuguler/cubicle
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 github:caglarutkuguler/cubicle install-hooks # once; backs up ~/.claude/settings.json first
31
- npx github:caglarutkuguler/cubicle --source claude-code
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 github:caglarutkuguler/cubicle --source ./agents.json
38
- npx github:caglarutkuguler/cubicle --source http://localhost:8080/agents
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 github:caglarutkuguler/cubicle
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 github:caglarutkuguler/cubicle --paperclip https://paperclip.example.com --token-file ~/.config/cubicle/paperclip-token
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 github:caglarutkuguler/cubicle` (Paperclip) veya `npx github: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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caglarutkuguler/cubicle",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A live pixel-art office for your AI agents — Paperclip, Claude Code, or any JSON feed. Zero dependencies, read-only.",
5
5
  "bin": {
6
6
  "cubicle": "bin/cubicle.js"
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:9px; padding:2px 7px; font-size:11px; font-weight:600; margin-bottom:3px; box-shadow:0 1px 0 rgba(0,0,0,.3); max-width:210px; overflow:hidden; text-overflow:ellipsis; }
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:11px; font-weight:600; padding:1px 6px; border-radius:5px; }
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
- const lang = (params.get('lang') || navigator.language || 'en').slice(0, 2).toLowerCase() === 'tr' ? 'tr' : 'en';
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
- statusEl.textContent = `${L.summary(agents.length, running)} · ${new Date().toLocaleTimeString(lang)}`;
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 = ''; }