Agent Lamp in motion

Agent Lamp

One local WLED lamp for GitHub Copilot desktop, Copilot CLI and Claude Code. A Node.js bridge tracks sessions separately and cycles through the sources that need attention. The agent never decides the lamp color.

This first version includes a hardware-free simulator, command-hook adapters, a desktop extension template and original printable lamp parts. Nothing installs into your agent settings, scans your network, flashes a board or creates a startup service. The application, tests and current CAD generator are strict TypeScript, compiled for Node.js 22 or later. The compiler, CAD and font tools are development dependencies; the running bridge has no third-party runtime dependencies.

Lamp behavior

Priority Color Meaning
1 Purple #A855F7 Copilot desktop needs attention
1 Cyan #06B6D4 Copilot CLI needs attention
1 Orange #F97316 Claude Code needs attention
2 Red #EF4444 A failure was observed
3 Gray #808080 State is unknown, stale or disconnected
4 Blue #2563EB Work was observed
5 Green #22C55E, briefly A response ended, not proof of success
6 Off No higher-priority signal

The three source hues are editable defaults, not user-selected colors. Set them in the local configuration. Defaults cycle every 1.2 seconds, once per waiting source, regardless of how many sessions it has. The timer keeps cycling without new events. When the current source stops waiting, the scheduler selects another waiting source immediately. Physical output can lag by the current bounded request and the 250 ms output rate limit.

Activity, another session finishing, errors, reconnecting, new prompts and idle events never erase outstanding attention. Steering messages are not treated as answers. Exact request-completion events clear only their matching request. Explicit cancellation, session termination and manual lamp acknowledgement clear that session. A failed termination retains unresolved attention because its outcome is uncertain.

Five minutes without a hook event makes terminal session state uncertain. Desktop observers send a connection heartbeat every 30 seconds. No timer clears an approval. A stale waiting session keeps its source color and reports uncertain: true in status. The lamp cannot separately encode every uncertainty; inspect the status command when a color persists.

Start without hardware

node --version
npm ci
npm run build
npm test
npm run demo

The demo starts a loopback bridge and a mock WLED HTTP server on temporary ports. It creates four sessions across three sources, waits without sending events, then resolves their requests. It does not read local configuration or contact your LAN.

Interactive Three.js assembly

npm run preview

Open http://127.0.0.1:43822. This starts a foreground, read-only local viewer after building its TypeScript browser bundle and preparing the print kit. Stop it with Ctrl+C. If the port is occupied, choose another with AGENT_LAMP_PREVIEW_PORT=43824 npm run preview.

The viewer loads the actual four capsule STLs and their exported print-to-assembly transforms. It animates the diffuser lifting, the LED deck separating, and the cassette sliding out. You can pause, use the assembly slider, orbit/zoom, select or isolate a part, inspect the lettering from above, and download the same files.

Choose Electronics to add representative ESP8266, AHCT buffer, capacitor, resistor, 5 V terminals and eight-pixel strip models. The modules and routed wires move with their cassette/deck; the connector separates rather than stretching a cable across the exploded view. These are illustrative placements and functional routes, not measured hardware or pin assignments, and they are not included in the STL files. The direct view URL is http://127.0.0.1:43822/?electronics=1.

Light colors and optional engraving paint fill are visual simulations, not a connection to the bridge or physical lamp. Fasteners are not rendered. The viewer respects reduced motion, stops animation when hidden/offscreen, and renders on demand when paused. It needs a browser with WebGL 2.

All scripts, fonts, geometry and artwork are served locally from an explicit asset allowlist. The preview has no event/control endpoint and never reads the bridge token or user agent settings. Its source is under preview/; npm run build bundles the browser code into dist/preview/, and npm run build:site exports the complete static site into dist/site/. After changing CAD, run npm run cad:capsule before restarting the preview so geometry, artwork and assembly metadata stay aligned.

Hosted viewer and downloadable files

Open the live viewer, electronics view or files and build guides.

The GitHub repository contains the source, tests, CAD generator and models. The static site publishes the same animation, illustrative electronics, rendered guides, individual STLs, editable parameters and print-kit ZIP. Repository visibility is separate from website access.

npm run build:site

This generates only public assets in dist/site. The Vercel configuration builds and serves that directory, not the wider dist tree. No bridge endpoint, token, agent data or generated local extension is deployed. Python 3.9 or later is needed to export the print kit during this build. See deployment instructions and release checks.

The hosted animation cannot drive WLED or observe agents. Install and run the bridge locally for that.

Run the status bridge

For a foreground bridge:

npm run init
npm start

In another terminal, from this checkout:

npm run lamp -- emit copilot-cli example attention approval
npm run lamp -- emit claude-code example attention question question
npm run status
npm run lamp -- emit copilot-cli example resolved approval
npm run lamp -- emit claude-code example resolved question question

init creates .agent-lamp/config.json with random bridge authentication and identity-hashing keys, private file permissions and dry-run output. It refuses to overwrite an existing configuration. Keep that directory private and uncommitted. Configuration paths resolve from --config FILE, then AGENT_LAMP_CONFIG, then .agent-lamp/config.json.

Stop the bridge with Ctrl+C. Attention state persists in .agent-lamp/state.json. On restart, restored sessions are marked disconnected until new evidence arrives. Do not delete or rotate the identity key casually: doing so changes the source pseudonyms and leaves old records needing manual reconciliation.

To clear a lamp record after independently checking or cancelling the request in the agent:

npm run lamp -- ack copilot-cli HASH_FROM_STATUS

This acknowledges only the local lamp record. It cannot approve a tool, answer a question or cancel agent work. Acknowledgement clears all outstanding requests in that one source/session, so inspect them first.

Source and build layout

Edit .ts files under src/, bin/, test/, scripts/, preview/ and integrations/. npm run build type-checks and compiles them to the ignored dist/ directory, and bundles the Three.js viewer for the browser. npm run typecheck checks without emitting, and npm test builds before running the compiled tests. After editing source, rebuild before using the CLI or installed adapters.

You use npm commands, not individual JavaScript source files. The one .mjs output is dist/integrations/copilot-desktop/extension.mjs, generated because the Copilot SDK requires that exact entry filename. Its maintained source is TypeScript, and nothing installs it automatically.

CAD parameters and generated STL files live under cad/, with the TypeScript generator under scripts/, so a design change stays with its models. Printing does not require those sources, Node.js or Python. Use the print-only kit below to get the parts and illustrated instructions without application code.

Install adapters only when you choose

Read adapter setup and limitations before enabling hooks. Use absolute paths to a durable copy of this project, not a worktree you will later remove. Keep the bridge running independently in the foreground.

Generate configuration snippets, without reading or modifying existing agent settings:

npm run --silent template:copilot
npm run --silent template:claude

Templates include absolute paths to the current Node executable, helper and local config. They contain no secrets. They use an observational shell wrapper that reports failure on stderr and exits zero, including when Node or the helper is missing. Helpers emit no stdout and have a 1.3-second overall deadline, a 500 ms input timeout and a 600 ms bridge timeout. Hook failures are diagnostic messages, never permission decisions or model context.

Source Attention support Resolution limits
Copilot desktop SDK permission prompts with a prompt descriptor and no resolvedByHook; user-input and elicitation requests Matching *.completed events clear matching request IDs. No live desktop installation was performed.
Copilot CLI Actual permission_prompt and elicitation_dialog notifications Public command-hook notifications lack request IDs and a universal resolution event. Those alerts remain latched until termination, explicit cancellation or manual ack.
Claude Code Delayed permission/MCP notifications and AskUserQuestion tool requests Question results clear matching tool IDs. Uncorrelated permission/MCP notifications remain latched. ElicitationResult cannot safely identify which generic notification to clear.

The conservative terminal behavior is deliberate. A post-tool event might belong to a parallel tool while another approval is still open. Turning off that alert would be worse than requiring an explicit acknowledgement.

Connect WLED later

WLED is independent firmware, not included here. The ESP8266 ESP-12F D1 mini-compatible clone with 4 MB flash is a reasonable small-prototype target. WLED publishes ESP8266 builds, but ESP8266 support is approaching its end. Confirm the exact board and current firmware instructions before flashing. This project does not flash it.

After separately setting up WLED on your trusted LAN, enter its known private IPv4 address in .agent-lamp/config.json:

{
  "mode": "wled",
  "wledUrl": "http://192.168.1.42",
  "brightness": 32
}

Change only those fields in the generated file; retain the other settings and keys. Restart the bridge. The example IP is not a detected device.

Configure WLED for eight connected WS2812B pixels, one segment with ID 0, RGB color order as verified on your strip, and the GPIO verified for your clone. Clear or disable other segments, playlists, live-data receivers and competing controllers. D4/GPIO2 is common on D1 mini boards, not a confirmed pin assignment for yours. Set a conservative WLED current limit for the actual wiring.

The adapter sends solid-color POST /json/state commands with brightness 32 out of 255 by default. This version caps configurable brightness at 64. It sets fx: 0 and zero transition rather than depending on version-specific animated effect IDs. It never saves presets during operation.

Only literal private IPv4 or loopback HTTP endpoints are accepted. No DNS discovery, redirects, public destinations or embedded credentials. WLED receives lighting data only. Requests serialize and coalesce to the newest desired state, time out after 700 ms and retry no faster than every two seconds. A 15-second refresh helps recover from a WLED reboot or a timed-out command that was applied anyway. WLED has no conditional sequence API, so a delayed device-side application cannot be ruled out after a timeout. The bridge reasserts the current command; it does not claim reliable hardware acknowledgement from an HTTP timeout.

WLED's HTTP API is not an internet-facing authenticated service. Do not port-forward it or expose it through a public tunnel. Brightness and software current limits do not make unsafe wiring safe.

Capsule STL files and no-solder assembly

The selected capsule has agent needs your attention and signal-ripple artwork recessed into its translucent top, a separate LED deck, and an organized removable cassette. Use the current capsule build guide for the exploded view, print orientation, support requirements and no-solder wiring precautions. The four design options remain available for reference.

This is an original adjustable fit-check prototype, not a confirmed fit for the user's board or modules. Only low-voltage modules belong in the cassette; the mains power supply stays external. The design is not derived from Octolamp and contains no copied models or branding.

To open the four capsule parts in Bambu Studio on macOS, run:

npm run print:open

This prepares dist/print-kit/ and opens the four STLs using Bambu Studio. Import in millimeters as separate objects and arrange them across the plates your printer requires. It does not slice or print. The export also creates dist/Agent-Lamp-print-kit.zip with the STLs, manifest, build guide and illustrations, without agent configuration, source code or printer presets.

To export without opening an app, or choose a location outside the developer project:

npm run print:export
npm run print:export -- --output-dir "$HOME/Downloads/Agent-Lamp"

Export requires Python 3.9 or later; opening the exported STLs does not. The specified output folder contains generated print-kit files that the command replaces on subsequent runs. A Copilot file link may open a code-editor tab instead of your slicer. In that case use the command above, or drag the actual STL files from Finder into Bambu Studio.

The assembled case is about 180 x 80 x 58 mm. The shell needs supports beneath the cassette doorway and shelves. The diffuser prints lettered-face-down, then flips for assembly. Default clearance is 0.6 mm, with four M3 lid fasteners and two M3 cassette fasteners. The slots allow adjustable retention of preassembled carriers; they are not fitted PCB mounting holes.

Regenerate the current capsule from its maintained TypeScript CAD source:

npm run cad:capsule
npm run design:options
python3 -m unittest discover -s cad -p 'test_*.py'

The generator checks connected solids, assembly intersections, rigid cassette travel and actual engraved volume before exporting binary STLs. Separate tests check the exported topology. None of this verifies real hardware dimensions, cable travel, temperature or a printer profile.

Bambu Studio 02.08.02.60's isolated command-line importer accepts the capsule's four STL meshes. An optional importer regression check runs with:

AGENT_LAMP_BAMBU_CLI=/Applications/BambuStudio.app/Contents/MacOS/BambuStudio \
  python3 -m unittest discover -s cad -p 'test_*.py'

That check uses a temporary Bambu configuration directory and does not slice, select a physical printer or print.

The earlier rectangular 3MF, Python generator and legacy guide remain for reference. They are not the capsule and are not included in the current print kit.

Privacy, boundaries and known gaps

Source adapters discard prompts, code, transcripts, paths, tool arguments/results, notification prose, credentials and error stacks before sending anything. They HMAC session/request/event identifiers with a local key. The bridge stores only fixed event kinds, source, opaque IDs, timing and attention categories. Raw hook JSON exists transiently in the helper's memory, capped at 1 MiB, and is never logged or stored. Oversized payloads are dropped with a diagnostic.

The bridge binds only to 127.0.0.1. Every endpoint requires a bearer token. Exact Host checks, Origin/browser rejection, no CORS, a 2 KiB event cap, schema allowlisting, connection limits and an authenticated rate limit reduce accidental web access. This is not isolation from malicious code already running as your OS user: that code can read your local config. Don't share it with an untrusted agent checkout or make it world-readable.

Events are untrusted. Host timestamps are used where documented; timestamps more than five seconds ahead of the bridge are rejected. Request-specific tombstones suppress older requests and completion-before-request replays. Cancellation/acknowledgement/termination establish replay boundaries, with closure winning equal-millisecond ties. A delayed session-wide closure is ignored if newer request evidence exists. Routine status rejects older events, while an older outstanding request can still arrive after unrelated activity.

Claude command hooks do not document a global event timestamp or sequence. The helper uses invocation time, with stable IDs for correlated tool events where available. Indistinguishable delayed notifications can re-latch an alert. A late old SessionEnd cannot always be distinguished from a new one after resume. No adapter reads transcripts to guess. CLI hook deliveries lost while the bridge is unavailable are not retried from disk. Desktop has a bounded in-memory retry queue, but crashes, overflow, missed pre-attachment events and bridge outages can still leave incomplete state. These are indicators, not an authoritative task monitor.

The desktop template observes the foreground SDK session to which its extension process attaches. It does not discover all other open app sessions, remote cloud jobs or LAN devices. Enable it in each intended desktop session's supported extension scope. Hooks may also run for subagents; main-stop events with explicit subagent IDs are ignored and request IDs remain separate. Host versions that omit child identity can still misclassify activity or idle, never erase an outstanding request.

Storage is bounded at 512 source/session records and 256 distinct request IDs per session. Capacity exhaustion reports an error rather than silently discarding approvals. For a long-running installation, stop the bridge, archive the private state file and start a fresh one only after reconciling all pending requests. No automatic garbage collection erases unknown waits.

No live agent end-to-end or physical lamp behavior is claimed. Automated coverage uses normalized events, SDK event fixtures, real loopback HTTP, subprocess helpers and generated mesh/package inspection.

References