liteagents 2.6.1 → 2.8.1
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 +58 -0
- package/README.md +4 -2
- package/installer/cli.js +4 -1
- package/package.json +1 -1
- package/packages/ampcode/commands/live-canvas/README.md +38 -47
- package/packages/{claude/skills/live-canvas/templates/demo → ampcode/commands/live-canvas/dev}/post-variants.html +8 -3
- package/packages/ampcode/commands/live-canvas/templates/lab-banner.html +6 -0
- package/packages/ampcode/commands/live-canvas/templates/overlay-vanilla.js +64 -6
- package/packages/ampcode/commands/live-canvas.md +140 -161
- package/packages/ampcode/commands/security.md +50 -12
- package/packages/ampcode/commands/ship.md +33 -11
- package/packages/claude/commands/git-commit.md +1 -1
- package/packages/claude/commands/security.md +50 -12
- package/packages/claude/commands/ship.md +33 -11
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/.claude-plugin/plugin.json +1 -1
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/README.md +2 -2
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/package.json +1 -1
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/schema.json +1 -1
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/server.js +320 -27
- package/packages/claude/plugins/live-canvas-marketplace/setup.sh +83 -24
- package/packages/claude/skills/live-canvas/README.md +53 -57
- package/packages/claude/skills/live-canvas/SKILL.md +122 -158
- package/packages/{ampcode/commands/live-canvas/templates/demo → claude/skills/live-canvas/dev}/post-variants.html +8 -3
- package/packages/claude/skills/live-canvas/templates/lab-banner.html +6 -0
- package/packages/claude/skills/live-canvas/templates/overlay-vanilla.js +64 -6
- package/packages/droid/commands/live-canvas/README.md +38 -47
- package/packages/droid/commands/live-canvas/{templates/demo → dev}/post-variants.html +8 -3
- package/packages/droid/commands/live-canvas/templates/lab-banner.html +6 -0
- package/packages/droid/commands/live-canvas/templates/overlay-vanilla.js +64 -6
- package/packages/droid/commands/live-canvas.md +140 -161
- package/packages/droid/commands/security.md +50 -12
- package/packages/droid/commands/ship.md +33 -11
- package/packages/opencode/command/live-canvas/README.md +38 -47
- package/packages/opencode/command/live-canvas/{templates/demo → dev}/post-variants.html +8 -3
- package/packages/opencode/command/live-canvas/templates/lab-banner.html +6 -0
- package/packages/opencode/command/live-canvas/templates/overlay-vanilla.js +64 -6
- package/packages/opencode/command/live-canvas.md +140 -161
- package/packages/opencode/command/security.md +50 -12
- package/packages/opencode/command/ship.md +33 -11
- package/packages/ampcode/commands/live-canvas/templates/.claude/settings.local.json +0 -8
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +0 -1299
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/format-utils.ts +0 -283
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/index.ts +0 -62
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/selector-utils.ts +0 -423
- package/packages/ampcode/commands/live-canvas/templates/feedback-react/types.ts +0 -118
- package/packages/claude/skills/live-canvas/INTEGRATION_NOTES.md +0 -210
- package/packages/claude/skills/live-canvas/templates/.claude/settings.local.json +0 -8
- package/packages/claude/skills/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +0 -1299
- package/packages/claude/skills/live-canvas/templates/feedback-react/format-utils.ts +0 -283
- package/packages/claude/skills/live-canvas/templates/feedback-react/index.ts +0 -62
- package/packages/claude/skills/live-canvas/templates/feedback-react/selector-utils.ts +0 -423
- package/packages/claude/skills/live-canvas/templates/feedback-react/types.ts +0 -118
- package/packages/droid/commands/live-canvas/templates/.claude/settings.local.json +0 -8
- package/packages/droid/commands/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +0 -1299
- package/packages/droid/commands/live-canvas/templates/feedback-react/format-utils.ts +0 -283
- package/packages/droid/commands/live-canvas/templates/feedback-react/index.ts +0 -62
- package/packages/droid/commands/live-canvas/templates/feedback-react/selector-utils.ts +0 -423
- package/packages/droid/commands/live-canvas/templates/feedback-react/types.ts +0 -118
- package/packages/opencode/command/live-canvas/templates/.claude/settings.local.json +0 -8
- package/packages/opencode/command/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +0 -1299
- package/packages/opencode/command/live-canvas/templates/feedback-react/format-utils.ts +0 -283
- package/packages/opencode/command/live-canvas/templates/feedback-react/index.ts +0 -62
- package/packages/opencode/command/live-canvas/templates/feedback-react/selector-utils.ts +0 -423
- package/packages/opencode/command/live-canvas/templates/feedback-react/types.ts +0 -118
|
@@ -2,17 +2,39 @@
|
|
|
2
2
|
name: ship
|
|
3
3
|
description: Check pre-deployment
|
|
4
4
|
usage: /ship
|
|
5
|
-
allowed-tools: Bash(npm *), Bash(
|
|
5
|
+
allowed-tools: Read, Grep, Glob, Bash(git *), Bash(npm *), Bash(pnpm *), Bash(yarn *), Bash(pytest *), Bash(python *), Bash(go *), Bash(cargo *), Bash(make *)
|
|
6
6
|
---
|
|
7
|
-
Pre-deploy
|
|
7
|
+
Pre-deploy / pre-merge gate. **Detect the stack first** (look for
|
|
8
|
+
`package.json`, `pyproject.toml`/`setup.cfg`, `go.mod`, `Cargo.toml`,
|
|
9
|
+
`Makefile`) and run only the checks that actually exist — never assume a
|
|
10
|
+
script (`lint`, `build`, `migrate`) is present. Report each item as
|
|
11
|
+
**pass / fail / N/A**.
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
- [ ]
|
|
11
|
-
|
|
12
|
-
- [ ]
|
|
13
|
-
- [ ]
|
|
14
|
-
- [ ] No
|
|
15
|
-
-
|
|
16
|
-
- [ ]
|
|
13
|
+
## Checklist
|
|
14
|
+
- [ ] **Tests pass** — run the project's real test command (`npm test`,
|
|
15
|
+
`pytest`, `go test ./...`, `cargo test`, `make test`).
|
|
16
|
+
- [ ] **Lint / format clean** — only if a linter or formatter is configured.
|
|
17
|
+
- [ ] **Build succeeds** — only if the project has a build step.
|
|
18
|
+
- [ ] **No debug leftovers** — stray `console.log` / `print` / `debugger` /
|
|
19
|
+
`dbg!` / commented-out blocks / blocker `TODO`s in the changed files.
|
|
20
|
+
- [ ] **No hardcoded secrets** — scan the diff. Secrets load from env / a
|
|
21
|
+
secret store; `.env` is gitignored and only a value-less `.env.example`
|
|
22
|
+
is tracked.
|
|
23
|
+
- [ ] **Error handling complete** — every new IO / network / DB call has a
|
|
24
|
+
failure path; nothing fails silently; no internal detail leaks to clients.
|
|
25
|
+
- [ ] **Authorization** — new endpoints/actions check **ownership + role**,
|
|
26
|
+
not just authentication (no IDOR via id-swapping).
|
|
27
|
+
- [ ] **Rate limiting** — new externally reachable routes, including
|
|
28
|
+
authenticated writes, are bounded.
|
|
29
|
+
- [ ] **Data access scoped & scales** — new queries are constrained to the
|
|
30
|
+
requesting principal (no cross-tenant leak) and avoid obvious N+1 /
|
|
31
|
+
unindexed scans on hot paths.
|
|
32
|
+
- [ ] **Migrations ready** — only if the project has a schema / migrations.
|
|
33
|
+
- [ ] **Docs & config in sync** — `.env.example`, README, and any
|
|
34
|
+
threat-model / PRD updated for new config or new attack surface.
|
|
35
|
+
- [ ] **Clean tree, correct branch, in sync with `origin`.**
|
|
17
36
|
|
|
18
|
-
|
|
37
|
+
For any security-sensitive change in the diff, run **`/security`** on the
|
|
38
|
+
changed files before shipping.
|
|
39
|
+
|
|
40
|
+
Report: **Ready 🚀** or **Blocked 🛑** with the specific failing items.
|
|
@@ -3,19 +3,57 @@ name: security
|
|
|
3
3
|
description: Scan security [target]
|
|
4
4
|
usage: /security
|
|
5
5
|
argument-hint: [file, directory, or leave empty for full scan]
|
|
6
|
-
allowed-tools: Read, Grep, Glob
|
|
6
|
+
allowed-tools: Read, Grep, Glob, Bash(git log:*), Bash(git grep:*), Bash(rg:*)
|
|
7
7
|
---
|
|
8
|
-
Audit $ARGUMENTS for security vulnerabilities.
|
|
8
|
+
Audit $ARGUMENTS for security vulnerabilities. Adapt scope to what the target
|
|
9
|
+
actually is — a library, CLI, web app, and service won't all have every
|
|
10
|
+
category. Skip what genuinely doesn't apply; never invent findings to fill a
|
|
11
|
+
section.
|
|
9
12
|
|
|
10
|
-
##
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
13
|
+
## The recurring six (check every project, where applicable)
|
|
14
|
+
These show up in nearly every quickly-built app regardless of stack:
|
|
15
|
+
|
|
16
|
+
1. **Secrets in the repo.** Tokens / API keys / `.env` files committed to
|
|
17
|
+
tracked files or anywhere in git history. Verify `.env` is gitignored and
|
|
18
|
+
only a value-less `.env.example` is tracked; scan history (`git log -p`,
|
|
19
|
+
`git grep`) for leaked keys. Secrets must load from env / a secret store at
|
|
20
|
+
runtime — never hardcoded, never logged.
|
|
21
|
+
2. **Data-access authorization (tenant isolation).** Every record read or
|
|
22
|
+
written must be scoped to the requesting principal — via DB-level rules
|
|
23
|
+
(RLS / row policies) and/or application-layer ownership checks. Flag any
|
|
24
|
+
query that trusts a client-supplied id without an ownership or role gate,
|
|
25
|
+
and any table/collection with a policy that's too broad or missing.
|
|
26
|
+
3. **Rate limiting.** Every externally reachable endpoint and abuse-prone
|
|
27
|
+
inbound path is bounded — including **authenticated mutation/write routes**,
|
|
28
|
+
not just the obvious public GETs. Note any unbounded route.
|
|
29
|
+
4. **Error handling past the happy path.** Third-party / IO / DB failures are
|
|
30
|
+
caught; nothing fails silently; no internal detail (stack traces, queries,
|
|
31
|
+
secrets) leaks to the client. Background/async work has its own catch.
|
|
32
|
+
5. **Authorization beyond authentication (IDOR / privilege).** "Logged in" is
|
|
33
|
+
not "allowed to do this". Confirm ownership AND role/permission checks on
|
|
34
|
+
every state-changing or privileged action. Mentally swap an id in a request
|
|
35
|
+
— does it return 403, or does it leak/modify another user's data?
|
|
36
|
+
6. **Inefficient data access (N+1 / unindexed).** Queries inside loops,
|
|
37
|
+
per-render repeated calls, missing indexes on filtered/joined columns.
|
|
38
|
+
Correct but falls over under load — a real availability risk.
|
|
39
|
+
|
|
40
|
+
## Also scan for
|
|
41
|
+
- **Injection:** SQL, command, XSS, template, path traversal.
|
|
42
|
+
- **Auth/session:** weak token handling, CSRF, session fixation, predictable ids.
|
|
43
|
+
- **Trust boundaries:** spoofable headers (e.g. `X-Forwarded-For`) trusted
|
|
44
|
+
without a vetted proxy; unvalidated untrusted input (uploads, inbound mail,
|
|
45
|
+
webhooks); services bound to `0.0.0.0` that should be loopback-only.
|
|
46
|
+
- **Config:** debug mode on in prod, default creds, missing security headers,
|
|
47
|
+
permissive CORS.
|
|
48
|
+
- **Dependencies:** known CVEs; unmaintained or single-maintainer deps in
|
|
49
|
+
security-critical paths.
|
|
16
50
|
|
|
17
51
|
## Output
|
|
18
|
-
Severity-ranked findings with:
|
|
19
|
-
- Location (file:line)
|
|
20
|
-
- Risk
|
|
21
|
-
- Remediation
|
|
52
|
+
Severity-ranked findings (Critical → High → Medium → Low), each with:
|
|
53
|
+
- **Location** (`file:line`)
|
|
54
|
+
- **Risk** — what an attacker actually gains
|
|
55
|
+
- **Remediation** — concrete, minimal fix
|
|
56
|
+
|
|
57
|
+
End with: which of the six classes were checked and found **clean**, and any
|
|
58
|
+
marked **N/A** for this target — so the scan's coverage is auditable, not just
|
|
59
|
+
its hits.
|
|
@@ -2,17 +2,39 @@
|
|
|
2
2
|
name: ship
|
|
3
3
|
description: Check pre-deployment
|
|
4
4
|
usage: /ship
|
|
5
|
-
allowed-tools: Bash(npm
|
|
5
|
+
allowed-tools: Read, Grep, Glob, Bash(git:*), Bash(npm:*), Bash(pnpm:*), Bash(yarn:*), Bash(pytest:*), Bash(python:*), Bash(go:*), Bash(cargo:*), Bash(make:*)
|
|
6
6
|
---
|
|
7
|
-
Pre-deploy
|
|
7
|
+
Pre-deploy / pre-merge gate. **Detect the stack first** (look for
|
|
8
|
+
`package.json`, `pyproject.toml`/`setup.cfg`, `go.mod`, `Cargo.toml`,
|
|
9
|
+
`Makefile`) and run only the checks that actually exist — never assume a
|
|
10
|
+
script (`lint`, `build`, `migrate`) is present. Report each item as
|
|
11
|
+
**pass / fail / N/A**.
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
- [ ]
|
|
11
|
-
|
|
12
|
-
- [ ]
|
|
13
|
-
- [ ]
|
|
14
|
-
- [ ] No
|
|
15
|
-
-
|
|
16
|
-
- [ ]
|
|
13
|
+
## Checklist
|
|
14
|
+
- [ ] **Tests pass** — run the project's real test command (`npm test`,
|
|
15
|
+
`pytest`, `go test ./...`, `cargo test`, `make test`).
|
|
16
|
+
- [ ] **Lint / format clean** — only if a linter or formatter is configured.
|
|
17
|
+
- [ ] **Build succeeds** — only if the project has a build step.
|
|
18
|
+
- [ ] **No debug leftovers** — stray `console.log` / `print` / `debugger` /
|
|
19
|
+
`dbg!` / commented-out blocks / blocker `TODO`s in the changed files.
|
|
20
|
+
- [ ] **No hardcoded secrets** — scan the diff. Secrets load from env / a
|
|
21
|
+
secret store; `.env` is gitignored and only a value-less `.env.example`
|
|
22
|
+
is tracked.
|
|
23
|
+
- [ ] **Error handling complete** — every new IO / network / DB call has a
|
|
24
|
+
failure path; nothing fails silently; no internal detail leaks to clients.
|
|
25
|
+
- [ ] **Authorization** — new endpoints/actions check **ownership + role**,
|
|
26
|
+
not just authentication (no IDOR via id-swapping).
|
|
27
|
+
- [ ] **Rate limiting** — new externally reachable routes, including
|
|
28
|
+
authenticated writes, are bounded.
|
|
29
|
+
- [ ] **Data access scoped & scales** — new queries are constrained to the
|
|
30
|
+
requesting principal (no cross-tenant leak) and avoid obvious N+1 /
|
|
31
|
+
unindexed scans on hot paths.
|
|
32
|
+
- [ ] **Migrations ready** — only if the project has a schema / migrations.
|
|
33
|
+
- [ ] **Docs & config in sync** — `.env.example`, README, and any
|
|
34
|
+
threat-model / PRD updated for new config or new attack surface.
|
|
35
|
+
- [ ] **Clean tree, correct branch, in sync with `origin`.**
|
|
17
36
|
|
|
18
|
-
|
|
37
|
+
For any security-sensitive change in the diff, run **`/security`** on the
|
|
38
|
+
changed files before shipping.
|
|
39
|
+
|
|
40
|
+
Report: **Ready 🚀** or **Blocked 🛑** with the specific failing items.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "live-canvas-channel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Bridges the Live Canvas browser overlay to a Claude Code session. Each overlay Save becomes a claude/channel notification delivered over MCP.",
|
|
5
5
|
"author": { "name": "liteagents" },
|
|
6
6
|
"channels": [
|
package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Claude Code channel plugin that bridges the Live Canvas browser overlay to a run
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
- HTTP listener: `127.0.0.1:8788` (override with `LIVE_CANVAS_PORT`)
|
|
15
|
-
- `GET /health` — overlay probes this to
|
|
15
|
+
- `GET /health` — overlay probes this to confirm Live mode is reachable
|
|
16
16
|
- `POST /feedback` — wire-compatible with the overlay's v1.0 schema
|
|
17
17
|
- stdio: MCP protocol using `@modelcontextprotocol/sdk`
|
|
18
18
|
|
|
@@ -84,6 +84,6 @@ curl -X POST http://localhost:8788/feedback \
|
|
|
84
84
|
|
|
85
85
|
## Failure modes
|
|
86
86
|
|
|
87
|
-
- **Port in use** — another Claude session owns 8788. Logs to stderr and exits; overlay health probe fails; overlay falls back to
|
|
87
|
+
- **Port in use** — another Claude session owns 8788. Logs to stderr and exits; overlay health probe fails; overlay falls back to JSON mode silently. The skill itself never silently degrades — it stops and asks the user.
|
|
88
88
|
- **Invalid payload** — HTTP 400 with reason; no notification emitted.
|
|
89
89
|
- **MCP transport not connected** — HTTP call still returns 200 (so the overlay shows "pushed"), but stderr logs the drop. Avoids false-negative toasts.
|
package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/schema.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"title": "LiveCanvasFeedback",
|
|
4
|
-
"description": "Payload shape posted by the Live Canvas overlay to POST /feedback.
|
|
4
|
+
"description": "Payload shape posted by the Live Canvas overlay to POST /feedback. v1.0 schema produced by overlay-vanilla.js.",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["version", "target", "comment"],
|
|
7
7
|
"properties": {
|
package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/server.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* live-canvas-channel — MCP channel server for Claude Code.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* 1.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* Three duties:
|
|
6
|
+
* 1. MCP stdio server — connects at startup so `/mcp` shows green in
|
|
7
|
+
* every Claude session, even sessions that never touch live-canvas.
|
|
8
|
+
* 2. Tools `channel_open` / `channel_close` — lazily bind/release the
|
|
9
|
+
* HTTP listener on LIVE_CANVAS_PORT (default 8788). Only the session
|
|
10
|
+
* that calls `channel_open` owns the port; other sessions stay idle.
|
|
11
|
+
* 3. HTTP listener (only while open) — accepts feedback POSTs from the
|
|
12
|
+
* browser overlay and emits a `notifications/claude/channel`
|
|
13
|
+
* notification into the owning session's turn.
|
|
13
14
|
*
|
|
14
15
|
* Protocol reference: https://code.claude.com/docs/en/channels-reference
|
|
15
16
|
*/
|
|
@@ -17,19 +18,28 @@
|
|
|
17
18
|
'use strict';
|
|
18
19
|
|
|
19
20
|
const http = require('http');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
20
23
|
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
21
24
|
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
25
|
+
const {
|
|
26
|
+
ListToolsRequestSchema,
|
|
27
|
+
CallToolRequestSchema,
|
|
28
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
22
29
|
|
|
23
30
|
const PORT = Number(process.env.LIVE_CANVAS_PORT || 8788);
|
|
24
31
|
const SERVER_NAME = 'live-canvas';
|
|
25
|
-
const SERVER_VERSION = '0.
|
|
32
|
+
const SERVER_VERSION = '0.5.0';
|
|
26
33
|
|
|
27
34
|
// ---------- MCP server ----------
|
|
28
35
|
|
|
29
36
|
const mcp = new Server(
|
|
30
37
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
31
38
|
{
|
|
32
|
-
capabilities: {
|
|
39
|
+
capabilities: {
|
|
40
|
+
tools: {},
|
|
41
|
+
experimental: { 'claude/channel': {} },
|
|
42
|
+
},
|
|
33
43
|
instructions: [
|
|
34
44
|
'Events from the live-canvas channel arrive as <channel source="live-canvas" ...> tags.',
|
|
35
45
|
'Each event is a single click-to-annotate comment made by the user on a UI variant in their browser.',
|
|
@@ -38,6 +48,7 @@ const mcp = new Server(
|
|
|
38
48
|
'When one arrives: acknowledge the feedback briefly, then edit the corresponding variant file',
|
|
39
49
|
'in .claude-design/lab/ using the selector to locate the element. This is a one-way channel —',
|
|
40
50
|
'no reply tool; respond to the user in chat as you would any normal message.',
|
|
51
|
+
'Before entering Live mode, call the channel_open tool; on exit or abort, call channel_close.',
|
|
41
52
|
].join(' '),
|
|
42
53
|
}
|
|
43
54
|
);
|
|
@@ -80,7 +91,6 @@ function validate(payload) {
|
|
|
80
91
|
return null;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
// Human-readable body the assistant will see inside the Claude turn.
|
|
84
94
|
function formatContent(target, comment) {
|
|
85
95
|
const { variant, element, text } = comment;
|
|
86
96
|
const label = element.textContent ? `, ${element.tagName} with "${element.textContent}"` : '';
|
|
@@ -91,7 +101,6 @@ function formatContent(target, comment) {
|
|
|
91
101
|
].join('\n');
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
// All values in `meta` must be strings per the claude/channel contract.
|
|
95
104
|
function buildMeta(target, comment) {
|
|
96
105
|
return {
|
|
97
106
|
source: 'live-canvas',
|
|
@@ -103,7 +112,7 @@ function buildMeta(target, comment) {
|
|
|
103
112
|
};
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
// ---------- HTTP server ----------
|
|
115
|
+
// ---------- HTTP server (created but not bound) ----------
|
|
107
116
|
|
|
108
117
|
const server = http.createServer(async (req, res) => {
|
|
109
118
|
cors(res);
|
|
@@ -116,6 +125,27 @@ const server = http.createServer(async (req, res) => {
|
|
|
116
125
|
return;
|
|
117
126
|
}
|
|
118
127
|
|
|
128
|
+
if (req.method === 'POST' && req.url === '/feedback-jsonl') {
|
|
129
|
+
// JSON-mode batch endpoint: appends the submitted payload to
|
|
130
|
+
// <parent claude cwd>/.claude-design/feedback.jsonl so the user doesn't
|
|
131
|
+
// have to download a file and paste. No capability gate — JSON mode
|
|
132
|
+
// doesn't depend on the experimental channels flag.
|
|
133
|
+
try {
|
|
134
|
+
const raw = await readBody(req);
|
|
135
|
+
JSON.parse(raw); // validate JSON only — schema is overlay-defined
|
|
136
|
+
const dir = path.join(process.cwd(), '.claude-design');
|
|
137
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
138
|
+
const file = path.join(dir, 'feedback.jsonl');
|
|
139
|
+
fs.appendFileSync(file, raw.trim() + '\n');
|
|
140
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
141
|
+
res.end(JSON.stringify({ ok: true, path: file }));
|
|
142
|
+
} catch (e) {
|
|
143
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
144
|
+
res.end(JSON.stringify({ error: String(e.message || e) }));
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
119
149
|
if (req.method === 'POST' && req.url === '/feedback') {
|
|
120
150
|
try {
|
|
121
151
|
const raw = await readBody(req);
|
|
@@ -133,8 +163,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
133
163
|
buildMeta(payload.target, payload.comment)
|
|
134
164
|
);
|
|
135
165
|
} catch (e) {
|
|
136
|
-
// MCP transport not yet connected or closed — still ack the HTTP
|
|
137
|
-
// caller so the overlay doesn't false-negative.
|
|
138
166
|
process.stderr.write(`[live-canvas] notify failed: ${e.message || e}\n`);
|
|
139
167
|
}
|
|
140
168
|
|
|
@@ -150,28 +178,293 @@ const server = http.createServer(async (req, res) => {
|
|
|
150
178
|
res.writeHead(404); res.end();
|
|
151
179
|
});
|
|
152
180
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
181
|
+
// ---------- channel capability check ----------
|
|
182
|
+
|
|
183
|
+
// Channels are still an experimental Claude Code feature. They only deliver
|
|
184
|
+
// `<channel>` tags into sessions launched with --dangerously-load-development-channels.
|
|
185
|
+
// Without that flag, a plain `claude` session can still load the MCP and bind the
|
|
186
|
+
// port, but every notification we emit is silently dropped — producing the
|
|
187
|
+
// "POST 200, but nothing landed" black hole. Detect by inspecting the parent
|
|
188
|
+
// claude's command-line; refuse to bind from non-qualifying sessions.
|
|
189
|
+
const RELAUNCH_HINT = 'Restart this session with: live-claude (or: claude --dangerously-load-development-channels plugin:live-canvas-channel@live-canvas-marketplace)';
|
|
190
|
+
|
|
191
|
+
function parentHasChannelsFlag() {
|
|
192
|
+
const FLAG = '--dangerously-load-development-channels';
|
|
193
|
+
const ppid = process.ppid;
|
|
194
|
+
|
|
195
|
+
// Linux: /proc/<pid>/cmdline is NUL-separated argv. Fast, no subprocess.
|
|
196
|
+
try {
|
|
197
|
+
const raw = require('fs').readFileSync(`/proc/${ppid}/cmdline`, 'utf8');
|
|
198
|
+
return raw.split('\0').some((arg) => arg === FLAG);
|
|
199
|
+
} catch { /* fall through */ }
|
|
200
|
+
|
|
201
|
+
// macOS / BSD / any POSIX without /proc: `ps -p <pid> -o args=` prints the
|
|
202
|
+
// full command line. Whitespace splits are fine here — the flag is a single
|
|
203
|
+
// token with no quoting needed.
|
|
204
|
+
if (process.platform !== 'win32') {
|
|
205
|
+
try {
|
|
206
|
+
const out = require('child_process')
|
|
207
|
+
.execFileSync('ps', ['-p', String(ppid), '-o', 'args='], { encoding: 'utf8', timeout: 1000 });
|
|
208
|
+
return out.split(/\s+/).some((arg) => arg === FLAG);
|
|
209
|
+
} catch { /* fall through */ }
|
|
157
210
|
}
|
|
158
|
-
|
|
159
|
-
process
|
|
211
|
+
|
|
212
|
+
// Windows: `wmic process where processid=<pid> get commandline /value`
|
|
213
|
+
// emits "CommandLine=<full string>". We split on whitespace then match.
|
|
214
|
+
if (process.platform === 'win32') {
|
|
215
|
+
try {
|
|
216
|
+
const out = require('child_process')
|
|
217
|
+
.execFileSync('wmic', ['process', 'where', `processid=${ppid}`, 'get', 'commandline', '/value'], { encoding: 'utf8', timeout: 1500 });
|
|
218
|
+
return out.split(/\s+/).some((arg) => arg === FLAG);
|
|
219
|
+
} catch { /* fall through */ }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Couldn't determine — fail closed. Better a wrong "no_channel_capability"
|
|
223
|
+
// error than a silent black hole. User can read the message and rerun.
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---------- lazy bind / release ----------
|
|
228
|
+
|
|
229
|
+
// Resolve who's holding `port`. Tries `ss` first (fast on Linux), then `lsof`
|
|
230
|
+
// (cross-platform). Returns null if we can't tell.
|
|
231
|
+
function findPortHolder(port) {
|
|
232
|
+
const { execFileSync } = require('child_process');
|
|
233
|
+
try {
|
|
234
|
+
const out = execFileSync('ss', ['-lntpH', `sport = :${port}`], { encoding: 'utf8', timeout: 800 });
|
|
235
|
+
const m = out.match(/pid=(\d+)/);
|
|
236
|
+
if (m) return Number(m[1]);
|
|
237
|
+
} catch { /* not Linux or ss unavailable */ }
|
|
238
|
+
try {
|
|
239
|
+
const out = execFileSync('lsof', ['-iTCP:' + port, '-sTCP:LISTEN', '-t'], { encoding: 'utf8', timeout: 800 });
|
|
240
|
+
const first = out.trim().split('\n')[0];
|
|
241
|
+
const pid = Number(first);
|
|
242
|
+
if (pid > 0) return pid;
|
|
243
|
+
} catch { /* lsof missing */ }
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// True iff `pid` is another instance of THIS server.js running as the same uid.
|
|
248
|
+
// "Same plugin binary + same user" is the criterion for safe takeover — it can
|
|
249
|
+
// only be another /live-canvas in another of this user's Claude sessions.
|
|
250
|
+
function isMyPluginServer(pid) {
|
|
251
|
+
if (!pid || pid === process.pid) return false;
|
|
252
|
+
const myUid = (typeof process.getuid === 'function') ? process.getuid() : null;
|
|
253
|
+
|
|
254
|
+
// Linux: read /proc/<pid>/cmdline + /proc/<pid>/status
|
|
255
|
+
try {
|
|
256
|
+
const cmdline = require('fs').readFileSync(`/proc/${pid}/cmdline`, 'utf8');
|
|
257
|
+
if (!cmdline.split('\0').includes(__filename)) return false;
|
|
258
|
+
if (myUid !== null) {
|
|
259
|
+
const status = require('fs').readFileSync(`/proc/${pid}/status`, 'utf8');
|
|
260
|
+
const m = status.match(/^Uid:\s+(\d+)/m);
|
|
261
|
+
if (m && Number(m[1]) !== myUid) return false;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
} catch { /* fall through */ }
|
|
265
|
+
|
|
266
|
+
// macOS / BSD: ps args + ps user
|
|
267
|
+
try {
|
|
268
|
+
const { execFileSync } = require('child_process');
|
|
269
|
+
const args = execFileSync('ps', ['-p', String(pid), '-o', 'args='], { encoding: 'utf8', timeout: 800 });
|
|
270
|
+
if (!args.includes(__filename)) return false;
|
|
271
|
+
if (myUid !== null) {
|
|
272
|
+
const uidOut = execFileSync('ps', ['-p', String(pid), '-o', 'uid='], { encoding: 'utf8', timeout: 800 });
|
|
273
|
+
if (Number(uidOut.trim()) !== myUid) return false;
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
} catch { /* fall through */ }
|
|
277
|
+
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
282
|
+
|
|
283
|
+
// One bind attempt. Resolves on success/EADDRINUSE; rejects on other errors.
|
|
284
|
+
function tryBind() {
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
const onError = (err) => {
|
|
287
|
+
server.off('error', onError);
|
|
288
|
+
if (err.code === 'EADDRINUSE') resolve(false);
|
|
289
|
+
else reject(err);
|
|
290
|
+
};
|
|
291
|
+
server.once('error', onError);
|
|
292
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
293
|
+
server.off('error', onError);
|
|
294
|
+
resolve(true);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function openChannel() {
|
|
300
|
+
const cap = parentHasChannelsFlag();
|
|
301
|
+
if (cap === false) {
|
|
302
|
+
return {
|
|
303
|
+
status: 'no_channel_capability',
|
|
304
|
+
port: PORT,
|
|
305
|
+
message: `This Claude session was launched without --dangerously-load-development-channels — channel notifications would be silently dropped. ${RELAUNCH_HINT}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (server.listening) return { status: 'already_listening', port: PORT };
|
|
309
|
+
|
|
310
|
+
if (await tryBind()) {
|
|
311
|
+
process.stderr.write(`[live-canvas] listening on 127.0.0.1:${PORT}\n`);
|
|
312
|
+
return { status: 'opened', port: PORT };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Port busy. If the holder is another instance of this same plugin running
|
|
316
|
+
// as the same uid, take it over — same user, same binary, definitionally
|
|
317
|
+
// safe. (The flag gate above already authorized this caller for channels.)
|
|
318
|
+
const holder = findPortHolder(PORT);
|
|
319
|
+
if (holder && isMyPluginServer(holder)) {
|
|
320
|
+
process.stderr.write(`[live-canvas] taking over port ${PORT} from sibling pid ${holder}\n`);
|
|
321
|
+
try { process.kill(holder, 'SIGTERM'); } catch { /* already gone */ }
|
|
322
|
+
|
|
323
|
+
// Poll for the port to free (the sibling's shutdown closes its listener).
|
|
324
|
+
for (let i = 0; i < 15; i++) {
|
|
325
|
+
await sleep(100);
|
|
326
|
+
if (await tryBind()) {
|
|
327
|
+
process.stderr.write(`[live-canvas] listening on 127.0.0.1:${PORT} (took over from pid ${holder})\n`);
|
|
328
|
+
return { status: 'opened', port: PORT, took_over: holder };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Last resort — SIGKILL the holdout, one more try.
|
|
332
|
+
try { process.kill(holder, 'SIGKILL'); } catch {}
|
|
333
|
+
await sleep(200);
|
|
334
|
+
if (await tryBind()) {
|
|
335
|
+
process.stderr.write(`[live-canvas] listening on 127.0.0.1:${PORT} (force-killed pid ${holder})\n`);
|
|
336
|
+
return { status: 'opened', port: PORT, took_over: holder, force_killed: true };
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
status: 'in_use',
|
|
340
|
+
port: PORT,
|
|
341
|
+
holder_pid: holder,
|
|
342
|
+
message: `Tried to take over port ${PORT} from sibling pid ${holder} but it wouldn't release. Manual cleanup needed: kill -9 ${holder}`,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Held by something else (different plugin, different user, or unknown).
|
|
347
|
+
return {
|
|
348
|
+
status: 'in_use',
|
|
349
|
+
port: PORT,
|
|
350
|
+
holder_pid: holder,
|
|
351
|
+
message: holder
|
|
352
|
+
? `Port ${PORT} is held by pid ${holder} (not a live-canvas server). Stop that process or pick JSON mode. To inspect: ps -fp ${holder}`
|
|
353
|
+
: `Port ${PORT} is held by an unknown process. Find it with: ss -lntp | grep ${PORT}`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function closeChannel() {
|
|
358
|
+
if (!server.listening) {
|
|
359
|
+
return Promise.resolve({ status: 'not_listening', port: PORT });
|
|
360
|
+
}
|
|
361
|
+
return new Promise((resolve) => {
|
|
362
|
+
server.close(() => {
|
|
363
|
+
process.stderr.write(`[live-canvas] released port ${PORT}\n`);
|
|
364
|
+
resolve({ status: 'closed', port: PORT });
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// JSON-mode bind. No flag gate — channels aren't used, only /feedback-jsonl.
|
|
370
|
+
// Lets a plain `claude` session bind the port so overlay submissions write
|
|
371
|
+
// straight to <cwd>/.claude-design/feedback.jsonl instead of triggering a
|
|
372
|
+
// browser download.
|
|
373
|
+
function openBatch() {
|
|
374
|
+
if (server.listening) {
|
|
375
|
+
return Promise.resolve({ status: 'already_listening', port: PORT });
|
|
376
|
+
}
|
|
377
|
+
return new Promise((resolve, reject) => {
|
|
378
|
+
const onError = (err) => {
|
|
379
|
+
server.off('error', onError);
|
|
380
|
+
if (err.code === 'EADDRINUSE') {
|
|
381
|
+
resolve({
|
|
382
|
+
status: 'in_use',
|
|
383
|
+
port: PORT,
|
|
384
|
+
message: `Port ${PORT} is held by another live-canvas session. JSON-mode submissions in this session will fall back to browser download instead of writing to .claude-design/feedback.jsonl.`,
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
reject(err);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
server.once('error', onError);
|
|
391
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
392
|
+
server.off('error', onError);
|
|
393
|
+
process.stderr.write(`[live-canvas] listening on 127.0.0.1:${PORT} (batch-only, no channels)\n`);
|
|
394
|
+
resolve({ status: 'opened', port: PORT });
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------- tools ----------
|
|
400
|
+
|
|
401
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
402
|
+
tools: [
|
|
403
|
+
{
|
|
404
|
+
name: 'channel_open',
|
|
405
|
+
description: 'Bind the live-canvas HTTP listener on port 8788 so the browser overlay can POST feedback into this session. Call once when entering Live mode. Returns {status, port, message?, took_over?, holder_pid?}. status: "opened" (bound, possibly after taking over a sibling live-canvas server — `took_over` is set to the prior pid if so), "already_listening" (we already had it), "in_use" (held by something that is NOT another live-canvas; `holder_pid` and `message` describe it), or "no_channel_capability" (this session lacks --dangerously-load-development-channels — show the user the message and stop, do not proceed to Live mode).',
|
|
406
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'channel_close',
|
|
410
|
+
description: 'Release the live-canvas HTTP listener so another session can claim it. Call on /live-canvas cleanup, abort, or when the user is done with Live mode. Returns {status: "closed" | "not_listening", port}.',
|
|
411
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: 'batch_open',
|
|
415
|
+
description: 'Bind the live-canvas HTTP listener on port 8788 for JSON mode only (no channel notifications, no flag required). Call this in JSON mode so the browser overlay can POST submissions to /feedback-jsonl which writes to <cwd>/.claude-design/feedback.jsonl. Returns {status: "opened" | "already_listening" | "in_use", port, message?}. If status is "in_use", JSON submissions fall back to browser download.',
|
|
416
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
}));
|
|
420
|
+
|
|
421
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
422
|
+
const { name } = req.params;
|
|
423
|
+
let result;
|
|
424
|
+
if (name === 'channel_open') {
|
|
425
|
+
result = await openChannel();
|
|
426
|
+
} else if (name === 'channel_close') {
|
|
427
|
+
result = await closeChannel();
|
|
428
|
+
} else if (name === 'batch_open') {
|
|
429
|
+
result = await openBatch();
|
|
430
|
+
} else {
|
|
431
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
432
|
+
}
|
|
433
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
160
434
|
});
|
|
161
435
|
|
|
162
|
-
// ---------- startup ----------
|
|
436
|
+
// ---------- startup / shutdown ----------
|
|
163
437
|
|
|
164
438
|
async function main() {
|
|
165
439
|
const transport = new StdioServerTransport();
|
|
166
440
|
await mcp.connect(transport);
|
|
167
441
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
442
|
+
// When the MCP host disconnects (session closed, /reload-plugins), release
|
|
443
|
+
// the port if we were holding it. Without this the next bind hits EADDRINUSE.
|
|
444
|
+
let closing = false;
|
|
445
|
+
const shutdown = () => {
|
|
446
|
+
if (closing) return;
|
|
447
|
+
closing = true;
|
|
448
|
+
if (server.listening) {
|
|
449
|
+
server.close(() => process.exit(0));
|
|
450
|
+
setTimeout(() => process.exit(0), 500).unref();
|
|
451
|
+
} else {
|
|
452
|
+
process.exit(0);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
transport.onclose = shutdown;
|
|
456
|
+
process.stdin.on('end', shutdown);
|
|
457
|
+
process.stdin.on('close', shutdown);
|
|
171
458
|
}
|
|
172
459
|
|
|
173
|
-
process.on('SIGTERM', () => {
|
|
174
|
-
|
|
460
|
+
process.on('SIGTERM', () => {
|
|
461
|
+
if (server.listening) server.close(() => process.exit(0));
|
|
462
|
+
else process.exit(0);
|
|
463
|
+
});
|
|
464
|
+
process.on('SIGINT', () => {
|
|
465
|
+
if (server.listening) server.close(() => process.exit(0));
|
|
466
|
+
else process.exit(0);
|
|
467
|
+
});
|
|
175
468
|
|
|
176
469
|
main().catch((err) => {
|
|
177
470
|
process.stderr.write(`[live-canvas] fatal: ${err}\n`);
|