liteagents 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/LICENSE +202 -21
- package/README.md +6 -4
- package/installer/cli.js +4 -1
- package/package.json +2 -2
- 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/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-lock.json +13 -13
- 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/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/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
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`);
|
|
@@ -1,61 +1,120 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Live Canvas channel — one-time setup
|
|
2
|
+
# Live Canvas channel — one-time setup.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Copies the marketplace into ~/.claude/plugins/, installs the channel
|
|
5
|
+
# plugin's npm deps, and adds a `live-claude` shell function so the user
|
|
6
|
+
# can launch a Live-mode session with one word in a new terminal.
|
|
7
|
+
#
|
|
8
|
+
# Safe to re-run: existing install at ~/.claude/plugins/live-canvas-marketplace/
|
|
9
|
+
# is overwritten; the shell function is replaced in place via marker guards.
|
|
6
10
|
|
|
7
11
|
set -e
|
|
12
|
+
|
|
8
13
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
DEST_ROOT="${HOME}/.claude/plugins"
|
|
15
|
+
DEST="${DEST_ROOT}/live-canvas-marketplace"
|
|
16
|
+
PLUGIN_DIR="${DEST}/plugins/live-canvas-channel"
|
|
17
|
+
MARKER_BEGIN="# >>> live-canvas: live-claude function (managed by setup.sh) >>>"
|
|
18
|
+
MARKER_END="# <<< live-canvas: live-claude function <<<"
|
|
11
19
|
|
|
12
20
|
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
|
13
21
|
ok() { printf '\033[32m✓\033[0m %s\n' "$*"; }
|
|
14
22
|
warn() { printf '\033[33m!\033[0m %s\n' "$*"; }
|
|
23
|
+
err() { printf '\033[31m✗\033[0m %s\n' "$*"; }
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
# ---------- prereq checks ----------
|
|
26
|
+
|
|
27
|
+
if [ "$(id -u)" = "0" ] && [ -n "${SUDO_USER:-}" ]; then
|
|
28
|
+
err "Don't run setup.sh with sudo — it would install into root's home (\$HOME=$HOME)."
|
|
29
|
+
err "Rerun as your normal user: bash $0"
|
|
18
30
|
exit 1
|
|
19
31
|
fi
|
|
20
32
|
|
|
33
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
34
|
+
err "Node.js not found. Install Node >= 18: https://nodejs.org"
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
21
37
|
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
|
|
22
38
|
if [ "$NODE_MAJOR" -lt 18 ]; then
|
|
23
|
-
|
|
39
|
+
err "Node >= 18 required (found $(node -v))."
|
|
24
40
|
exit 1
|
|
25
41
|
fi
|
|
26
|
-
|
|
27
42
|
if ! command -v npm >/dev/null 2>&1; then
|
|
28
|
-
|
|
43
|
+
err "npm not found. Usually ships with Node."
|
|
29
44
|
exit 1
|
|
30
45
|
fi
|
|
31
46
|
|
|
47
|
+
# ---------- 1. copy marketplace into ~/.claude/plugins ----------
|
|
48
|
+
|
|
49
|
+
bold "Installing marketplace to ${DEST}…"
|
|
50
|
+
mkdir -p "$DEST_ROOT"
|
|
51
|
+
# Overwrite any prior install. Small, self-contained tree — safe to wipe.
|
|
52
|
+
rm -rf "$DEST"
|
|
53
|
+
cp -R "$HERE" "$DEST"
|
|
54
|
+
ok "Copied marketplace files."
|
|
55
|
+
|
|
56
|
+
# ---------- 2. npm install channel plugin deps ----------
|
|
57
|
+
|
|
32
58
|
bold "Installing channel plugin dependencies…"
|
|
33
59
|
cd "$PLUGIN_DIR"
|
|
34
60
|
npm install --silent
|
|
35
61
|
ok "Dependencies installed in $PLUGIN_DIR"
|
|
36
62
|
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
# ---------- 3. add live-claude function to shell rc files ----------
|
|
64
|
+
|
|
65
|
+
add_to_rc() {
|
|
66
|
+
local rc="$1"
|
|
67
|
+
[ -e "$rc" ] || return 0
|
|
68
|
+
|
|
69
|
+
local tmp
|
|
70
|
+
tmp="$(mktemp)"
|
|
71
|
+
# Strip any prior managed block, preserving the rest of the file verbatim.
|
|
72
|
+
awk -v b="$MARKER_BEGIN" -v e="$MARKER_END" '
|
|
73
|
+
$0 == b { skip = 1; next }
|
|
74
|
+
$0 == e { skip = 0; next }
|
|
75
|
+
!skip { print }
|
|
76
|
+
' "$rc" > "$tmp"
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
cat "$tmp"
|
|
80
|
+
echo ""
|
|
81
|
+
echo "$MARKER_BEGIN"
|
|
82
|
+
echo "live-claude() {"
|
|
83
|
+
echo " claude --dangerously-load-development-channels plugin:live-canvas-channel@live-canvas-marketplace \"\$@\""
|
|
84
|
+
echo "}"
|
|
85
|
+
echo "$MARKER_END"
|
|
86
|
+
} > "$rc.live-canvas.tmp"
|
|
87
|
+
|
|
88
|
+
mv "$rc.live-canvas.tmp" "$rc"
|
|
89
|
+
rm -f "$tmp"
|
|
90
|
+
ok "Updated $rc"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
bold "Adding live-claude function to your shell rc files…"
|
|
94
|
+
add_to_rc "$HOME/.zshrc"
|
|
95
|
+
add_to_rc "$HOME/.bashrc"
|
|
96
|
+
|
|
97
|
+
# ---------- 4. print remaining manual steps ----------
|
|
98
|
+
|
|
39
99
|
cat <<EOF
|
|
40
100
|
|
|
41
|
-
|
|
42
|
-
|
|
101
|
+
$(bold "Two more steps — run these inside Claude Code:")
|
|
102
|
+
|
|
103
|
+
1. Register the marketplace:
|
|
104
|
+
/plugin marketplace add ${DEST}
|
|
43
105
|
|
|
44
106
|
2. Install the plugin from it:
|
|
45
107
|
/plugin install live-canvas-channel@live-canvas-marketplace
|
|
46
108
|
|
|
47
|
-
|
|
48
|
-
claude --dangerously-load-development-channels plugin:live-canvas-channel@live-canvas-marketplace
|
|
109
|
+
$(bold "Then daily use:")
|
|
49
110
|
|
|
50
|
-
|
|
51
|
-
|
|
111
|
+
• Open a fresh shell (or: source ~/.zshrc) so live-claude is on PATH.
|
|
112
|
+
• Terminal A — your dev server (e.g. npm run dev).
|
|
113
|
+
• Terminal B — run: live-claude
|
|
114
|
+
(this is just: claude --dangerously-load-development-channels …)
|
|
115
|
+
• Inside that Claude session, run /live-canvas.
|
|
52
116
|
|
|
53
|
-
|
|
54
|
-
auto-detect the channel and use Live mode.
|
|
55
|
-
|
|
56
|
-
Tip — save yourself typing with a shell alias in ~/.zshrc or ~/.bashrc:
|
|
57
|
-
alias claude-live='claude --dangerously-load-development-channels plugin:live-canvas-channel@live-canvas-marketplace'
|
|
117
|
+
You'll be asked Live vs JSON mode each time.
|
|
58
118
|
|
|
59
119
|
EOF
|
|
60
|
-
|
|
61
120
|
ok "Setup complete."
|