@comty.io/core 0.1.4 → 0.1.6

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/src/cursor.mjs ADDED
@@ -0,0 +1,49 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { openAgentProcess } from './agent-process.mjs';
3
+ import { parseReceipt, receiptSchema, taskPrompt } from './codex.mjs';
4
+
5
+ export async function runCursor({ cwd, task, signal, approve, progress, model, executable = 'agent', args = [] }) {
6
+ cwd = realpathSync(cwd);
7
+ let output = '';
8
+ const rpc = openAgentProcess({
9
+ executable, args: [...args, ...(model ? ['--model', model] : []), 'acp'], cwd, signal, label: 'Cursor',
10
+ async onRequest(method, params) {
11
+ if (method === 'session/request_permission') {
12
+ const allow = await approve({ type: method, details: params.toolCall });
13
+ const option = params.options?.find(option => option.kind === (allow ? 'allow_once' : 'reject_once'));
14
+ return { outcome: option ? { outcome: 'selected', optionId: option.optionId } : { outcome: 'cancelled' } };
15
+ }
16
+ if (method === 'cursor/create_plan') {
17
+ return { outcome: { outcome: await approve({ type: method, details: params }) ? 'accepted' : 'rejected' } };
18
+ }
19
+ throw new Error(`Cursor needs an unsupported interaction: ${method}. Create a new task with the missing details.`);
20
+ },
21
+ onNotification(method, params) {
22
+ if (method !== 'session/update' || params?.update?.sessionUpdate !== 'agent_message_chunk') return;
23
+ if (params.update.content?.type === 'text') {
24
+ output += params.update.content.text;
25
+ if (output.length > 1024 * 1024) throw new Error('Cursor response exceeded 1 MiB');
26
+ progress(output.slice(-4000));
27
+ }
28
+ },
29
+ });
30
+ try {
31
+ await rpc.request('initialize', {
32
+ protocolVersion: 1, clientInfo: { name: 'comty', version: '0.1.0' },
33
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
34
+ });
35
+ await rpc.request('authenticate', { methodId: 'cursor_login' });
36
+ const session = await rpc.request('session/new', { cwd, mcpServers: [] });
37
+ if (!session?.sessionId) throw new Error('Cursor did not create a session');
38
+ await rpc.request('session/set_mode', { sessionId: session.sessionId, modeId: 'agent' });
39
+ const result = await rpc.request('session/prompt', {
40
+ sessionId: session.sessionId,
41
+ prompt: [{ type: 'text', text: `${taskPrompt(task)}\n\nEnd your final response with <comty_result>JSON</comty_result>. The JSON must match this schema: ${JSON.stringify(receiptSchema)}. Write nothing after the closing tag.` }],
42
+ }, 0);
43
+ if (result?.stopReason !== 'end_turn') throw new Error(`Cursor stopped: ${result?.stopReason || 'unknown reason'}`);
44
+ const text = output.trim();
45
+ const start = text.lastIndexOf('<comty_result>');
46
+ if (start < 0 || !text.endsWith('</comty_result>')) throw new Error('Cursor did not return a completion receipt');
47
+ return parseReceipt(text.slice(start + '<comty_result>'.length, -'</comty_result>'.length));
48
+ } finally { await rpc.close(); }
49
+ }
package/src/dev.mjs ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ import { createServer, request as httpRequest } from 'node:http';
3
+ import { spawn } from 'node:child_process';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { realpathSync } from 'node:fs';
6
+ import { resolve, delimiter } from 'node:path';
7
+ import { parseArgs } from 'node:util';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { createCompanion, isLoopback } from './companion.mjs';
10
+
11
+ export async function startDev({ cwd = process.cwd(), target = 'http://127.0.0.1:5173', port = 4387, provider = 'codex', model, projectKey, apiUrl, command = [] } = {}) {
12
+ const upstream = new URL(target);
13
+ if (upstream.protocol !== 'http:' || !isLoopback(upstream.hostname) || upstream.username || upstream.password || upstream.pathname !== '/' || upstream.search || upstream.hash) {
14
+ throw new Error('--target must be a loopback HTTP origin, e.g. http://127.0.0.1:5173');
15
+ }
16
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Invalid --port');
17
+ const runtime = await readFile(new URL('./browser.js', import.meta.url));
18
+ const style = await readFile(new URL('./style.js', import.meta.url));
19
+ const anchors = await readFile(new URL('./anchors.js', import.meta.url));
20
+ const capture = await readFile(new URL('./capture.js', import.meta.url));
21
+ const captureEngine = await readFile(new URL('./capture-engine.js', import.meta.url));
22
+ const captureResources = await readFile(new URL('./capture-resources.js', import.meta.url));
23
+ const comments = await readFile(new URL('./comments.js', import.meta.url));
24
+ const measurements = await readFile(new URL('./measurements.js', import.meta.url));
25
+ let companion, origin, child, closing, finished;
26
+ const sockets = new Set();
27
+ const proxies = new Set();
28
+ const server = createServer((request, response) => {
29
+ if (!origin || request.headers.host !== new URL(origin).host) {
30
+ response.writeHead(403); response.end('Comty is available on its loopback URL only'); return;
31
+ }
32
+ let url;
33
+ try { url = new URL(request.url, origin); }
34
+ catch { response.writeHead(400); response.end('Invalid request target'); return; }
35
+ if (url.origin !== origin) { response.writeHead(400); response.end('Invalid request target'); return; }
36
+ if (url.pathname.startsWith('/__comty/')) {
37
+ if (request.headers.origin && request.headers.origin !== origin) {
38
+ response.writeHead(403); response.end(); return;
39
+ }
40
+ if (request.method === 'GET' && ['/__comty/browser.js', '/__comty/style.js', '/__comty/anchors.js', '/__comty/capture.js', '/__comty/capture-engine.js', '/__comty/capture-resources.js', '/__comty/comments.js', '/__comty/measurements.js', '/__comty/start.js'].includes(url.pathname)) {
41
+ response.writeHead(200, { 'Content-Type': 'text/javascript', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
42
+ response.end(url.pathname.endsWith('/browser.js') ? runtime : url.pathname.endsWith('/style.js') ? style : url.pathname.endsWith('/anchors.js') ? anchors : url.pathname.endsWith('/capture.js') ? capture : url.pathname.endsWith('/capture-engine.js') ? captureEngine : url.pathname.endsWith('/capture-resources.js') ? captureResources : url.pathname.endsWith('/comments.js') ? comments : url.pathname.endsWith('/measurements.js') ? measurements :
43
+ `import { initComty } from '/__comty/browser.js';\ninitComty(${JSON.stringify({ projectKey, apiUrl, agent: { url: `${origin}/__comty`, token: companion.token } })}).catch(console.error);`);
44
+ return;
45
+ }
46
+ void companion.handle(request, response); return;
47
+ }
48
+ const destination = new URL(upstream);
49
+ destination.pathname = url.pathname; destination.search = url.search;
50
+ const proxy = httpRequest(destination, {
51
+ method: request.method,
52
+ headers: { ...request.headers, host: upstream.host, 'accept-encoding': 'identity' },
53
+ }, incoming => {
54
+ const headers = { ...incoming.headers };
55
+ if (headers.location?.startsWith(upstream.origin)) headers.location = headers.location.replace(upstream.origin, origin);
56
+ if (!headers['content-type']?.includes('text/html') || request.method === 'HEAD') {
57
+ response.writeHead(incoming.statusCode, headers); incoming.pipe(response); return;
58
+ }
59
+ // ponytail: buffer development HTML for injection; use the Vite integration for streamed HTML.
60
+ const chunks = []; let length = 0;
61
+ incoming.on('data', chunk => {
62
+ length += chunk.length;
63
+ if (length > 8 * 1024 * 1024) incoming.destroy(new Error('HTML exceeds the 8 MiB development limit'));
64
+ else chunks.push(chunk);
65
+ });
66
+ incoming.on('error', error => { if (!response.headersSent) response.writeHead(502); response.end(error.message); });
67
+ incoming.on('end', () => {
68
+ if (incoming.headers['content-encoding'] && incoming.headers['content-encoding'] !== 'identity') {
69
+ response.writeHead(502); response.end('Comty requires uncompressed development HTML'); return;
70
+ }
71
+ const html = Buffer.concat(chunks).toString('utf8');
72
+ const script = '<script type="module" src="/__comty/start.js"></script>';
73
+ const injected = /<head(?:\s[^>]*)?>/i.test(html) ? html.replace(/<head(?:\s[^>]*)?>/i, match => match + script) : script + html;
74
+ delete headers['content-length']; delete headers.etag; delete headers['content-encoding'];
75
+ headers['cache-control'] = 'no-store';
76
+ response.writeHead(incoming.statusCode, headers); response.end(injected);
77
+ });
78
+ });
79
+ proxies.add(proxy); proxy.on('close', () => proxies.delete(proxy));
80
+ proxy.on('error', () => {
81
+ if (!response.headersSent) response.writeHead(502, { 'Content-Type': 'text/plain' });
82
+ response.end(`Development server unavailable at ${upstream.origin}. Check its terminal output and reload.`);
83
+ });
84
+ request.on('aborted', () => proxy.destroy());
85
+ response.on('close', () => proxy.destroy());
86
+ request.pipe(proxy);
87
+ });
88
+ server.on('connection', socket => { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); });
89
+ server.on('upgrade', (request, socket, head) => {
90
+ if (request.headers.host !== new URL(origin).host || (request.headers.origin && request.headers.origin !== origin)) return socket.destroy();
91
+ let url;
92
+ try { url = new URL(request.url, origin); } catch { return socket.destroy(); }
93
+ if (url.origin !== origin || url.pathname.startsWith('/__comty/')) return socket.destroy();
94
+ const destination = new URL(upstream);
95
+ destination.pathname = url.pathname; destination.search = url.search;
96
+ const proxy = httpRequest(destination, { headers: { ...request.headers, host: upstream.host } });
97
+ proxies.add(proxy); proxy.on('close', () => proxies.delete(proxy));
98
+ proxy.on('upgrade', (response, upstreamSocket, upstreamHead) => {
99
+ sockets.add(upstreamSocket); upstreamSocket.on('close', () => sockets.delete(upstreamSocket));
100
+ socket.write(`HTTP/1.1 ${response.statusCode} ${response.statusMessage}\r\n${response.rawHeaders.reduce((text, value, index, all) => index % 2 ? text : text + value + ': ' + all[index + 1] + '\r\n', '')}\r\n`);
101
+ if (upstreamHead.length) socket.write(upstreamHead);
102
+ if (head.length) upstreamSocket.write(head);
103
+ socket.pipe(upstreamSocket).pipe(socket);
104
+ socket.on('error', () => upstreamSocket.destroy());
105
+ upstreamSocket.on('error', () => socket.destroy());
106
+ socket.on('close', () => upstreamSocket.destroy());
107
+ upstreamSocket.on('close', () => socket.destroy());
108
+ });
109
+ proxy.on('response', response => { response.resume(); socket.destroy(); });
110
+ proxy.on('error', () => socket.destroy());
111
+ proxy.end();
112
+ });
113
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, '127.0.0.1', resolve); });
114
+ origin = `http://127.0.0.1:${server.address().port}`;
115
+ if (new URL(origin).port === upstream.port) { server.close(); throw new Error('Comty and the application must use different ports'); }
116
+ try { companion = createCompanion({ cwd, origin, provider, model }); }
117
+ catch (error) { server.close(); throw error; }
118
+ async function close() {
119
+ if (closing) return closing;
120
+ closing = (async () => {
121
+ const kill = signal => {
122
+ if (!child?.pid) return;
123
+ try {
124
+ if (process.platform === 'win32') child.kill(signal);
125
+ else process.kill(-child.pid, signal);
126
+ } catch (error) { if (error.code !== 'ESRCH') throw error; }
127
+ };
128
+ kill('SIGTERM');
129
+ const force = setTimeout(() => kill('SIGKILL'), 2000);
130
+ for (const proxy of proxies) proxy.destroy();
131
+ for (const socket of sockets) socket.destroy();
132
+ await Promise.all([companion.close(), new Promise(resolve => server.close(resolve)),
133
+ child && child.exitCode === null && child.signalCode === null ? new Promise(resolve => child.once('close', resolve)) : Promise.resolve()]);
134
+ clearTimeout(force);
135
+ })();
136
+ return closing;
137
+ }
138
+ if (command.length) {
139
+ child = spawn(command[0], command.slice(1), {
140
+ cwd, stdio: 'inherit', detached: process.platform !== 'win32', env: { ...process.env, PATH: `${resolve(cwd, 'node_modules/.bin')}${delimiter}${process.env.PATH || ''}` },
141
+ });
142
+ child.on('error', error => { console.error(`Comty could not start ${command[0]}: ${error.message}`); });
143
+ finished = new Promise(resolve => child.once('close', code => resolve(Number.isInteger(code) && code >= 0 ? code : 1))).then(async code => {
144
+ await close();
145
+ return code;
146
+ });
147
+ finished.catch(error => console.error(`Comty shutdown failed: ${error.message}`));
148
+ }
149
+ return { origin, close, finished };
150
+ }
151
+
152
+ async function main() {
153
+ const args = process.argv.slice(2);
154
+ if (!args.length || args.includes('--help')) {
155
+ console.log('Usage: comty dev [--agent codex|claude|cursor|gemini|grok] [--model MODEL] [--target http://127.0.0.1:5173] [--port 4387] [--project-key KEY --api-url URL] -- vite --strictPort'); return;
156
+ }
157
+ if (args.shift() !== 'dev') throw new Error('Expected comty dev. Use --help for usage.');
158
+ const split = args.indexOf('--');
159
+ const { values } = parseArgs({ args: split < 0 ? args : args.slice(0, split), options: {
160
+ target: { type: 'string' }, port: { type: 'string' }, agent: { type: 'string' }, model: { type: 'string' }, 'project-key': { type: 'string' }, 'api-url': { type: 'string' },
161
+ } });
162
+ const session = await startDev({ target: values.target, port: values.port === undefined ? 4387 : Number(values.port), provider: values.agent, model: values.model,
163
+ projectKey: values['project-key'], apiUrl: values['api-url'], command: split < 0 ? [] : args.slice(split + 1) });
164
+ console.log(`Comty: ${session.origin} — open this URL to annotate with the local agent.`);
165
+ for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => { void session.close().catch(console.error); });
166
+ if (session.finished) process.exitCode = await session.finished;
167
+ }
168
+
169
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
170
+ main().catch(error => { console.error(`Comty: ${error.message}`); process.exitCode = 1; });
171
+ }
package/src/gemini.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { runAcp } from './acp.mjs';
2
+
3
+ export const runGemini = options => runAcp({ ...options, provider: 'gemini' });
package/src/grok.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { runAcp } from './acp.mjs';
2
+
3
+ export const runGrok = options => runAcp({ ...options, provider: 'grok' });
package/src/style.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export function getComtyStyle() {
2
2
  const toolbarSurface = "#2b2d2f";
3
3
  const toolbarMuted = "#b8babb";
4
- const commentCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><path d="M20 2C30 2 38 10 38 20S30 38 20 38H3V28C2 25 2 23 2 20 2 10 10 2 20 2Z" fill="white" stroke="#d6dbe2"/><circle cx="20" cy="20" r="14" fill="#e6e6e6"/><path d="M20 14v12m-6-6h12" fill="none" stroke="#404040" stroke-width="2.5" stroke-linecap="round"/></svg>`;
4
+ const commentCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 40 40"><path d="M20 2C30 2 38 10 38 20S30 38 20 38H3V28C2 25 2 23 2 20 2 10 10 2 20 2Z" fill="white" stroke="#d6dbe2"/><circle cx="20" cy="20" r="14" fill="#e6e6e6"/><path d="M20 14v12m-6-6h12" fill="none" stroke="#404040" stroke-width="2.5" stroke-linecap="round"/></svg>`;
5
5
  const typographyCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="8" y="8" width="23" height="23" rx="7" fill="${toolbarSurface}" stroke="${toolbarMuted}" stroke-width=".75"/><path d="m14 25 5.5-12L25 25m-8.5-4h6M13 25h4m5 0h4" fill="none" stroke="${toolbarMuted}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="m2 2 2 15 4-5 6-3Z" fill="${toolbarSurface}" stroke="${toolbarMuted}" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
6
6
  return `
7
7
  #comty-root {
@@ -11,7 +11,7 @@ export function getComtyStyle() {
11
11
  --comty-muted: var(--comty-toolbar-muted);
12
12
  --comty-accent: #55aaf5;
13
13
  --comty-error: #ef7676;
14
- --comty-outline: var(--comty-toolbar-muted);
14
+ --comty-outline: var(--comty-accent);
15
15
  --comty-comment-focus: var(--comty-toolbar-muted);
16
16
  --comty-paper: #fff;
17
17
  --comty-ink: #171717;
@@ -58,6 +58,8 @@ export function getComtyStyle() {
58
58
  --comty-dialog-width: 32rem;
59
59
  --comty-dialog-shadow: 0 1.5rem 4rem rgb(0 0 0 / .16);
60
60
  --comty-motion-duration: 220ms;
61
+ --comty-working-dot-size: .25em;
62
+ --comty-working-dot-gap: .125em;
61
63
  --comty-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
62
64
  --comty-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
63
65
  position: fixed;
@@ -69,13 +71,73 @@ export function getComtyStyle() {
69
71
  font-family: var(--comty-font);
70
72
  color-scheme: dark;
71
73
  }
72
-
74
+ #comty-root :is(button, .comty-comment-avatar) { user-select: none; -webkit-user-select: none; }
75
+ #comty-root .comty-comment-avatar img { -webkit-user-drag: none; }
76
+ #comty-root .comty-pin .comty-comment-avatar { pointer-events: none; }
77
+ #comty-root > .comty-agent-panel { position: fixed; right: 1rem; bottom: 5rem; z-index: 2147483647; width: min(22rem, calc(100vw - 2rem)); max-height: min(32rem, calc(100vh - 6rem)); box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; pointer-events: auto; border: .0625rem solid var(--comty-border); border-radius: 1rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .875rem/1.4 ui-sans-serif, system-ui, sans-serif; transform: translateY(0); opacity: 1; transition: transform var(--comty-motion-duration) var(--comty-ease-out), opacity var(--comty-motion-duration) ease; color-scheme: dark; }
78
+ #comty-root > .comty-agent-panel[data-side="left"] { left: 1rem; right: auto; }
79
+ #comty-root > .comty-agent-panel[data-closed] { transform: translateY(.5rem); opacity: 0; pointer-events: none; }
80
+ #comty-root > .comty-agent-panel[hidden] { display: none; }
81
+ .comty-agent-panel *, .comty-agent-panel *::before { box-sizing: border-box; }
82
+ .comty-agent-panel-header { display: flex; flex: 0 0 auto; align-items: center; gap: .75rem; padding: .5rem .75rem; border-bottom: .0625rem solid var(--comty-hover-surface); }
83
+ .comty-agent-brand { display: grid; place-items: center; width: 2.375rem; height: 2.375rem; flex-shrink: 0; border-radius: .6875rem; color: var(--comty-accent); background: var(--comty-hover-surface); }
84
+ .comty-agent-brand.comty-agent-avatar { width: 2.375rem; height: 2.375rem; border-radius: 0; background: transparent; }
85
+ .comty-agent-panel svg { display: block; width: 1rem; height: 1rem; flex-shrink: 0; }
86
+ .comty-agent-brand svg { width: 1.25rem; height: 1.25rem; }
87
+ .comty-agent-brand.comty-agent-avatar svg { width: 100%; height: 100%; }
88
+ .comty-agent-answer { display: flex; align-items: flex-start; gap: .5rem; padding: .375rem .5rem .875rem; }
89
+ .comty-agent-answer .comty-agent-brand { width: 1.5rem; height: 1.5rem; }
90
+ .comty-agent-answer p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--comty-foreground); }
91
+ .comty-agent-heading { flex: 1; min-width: 0; }
92
+ .comty-agent-heading strong { display: block; font-weight: 600; font-size: .875rem; }
93
+ .comty-agent-panel-content { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--comty-hover-surface) transparent; padding: .375rem .5rem 2rem; }
94
+ .comty-agent-item { padding: .375rem .25rem; overflow-wrap: anywhere; }
95
+ .comty-agent-item + .comty-agent-item { border-top: .0625rem solid var(--comty-hover-surface); }
96
+ .comty-agent-item h3 { margin: 0 0 .25rem; color: var(--comty-foreground); font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; }
97
+ .comty-agent-list { display: grid; gap: .125rem; }
98
+ .comty-agent-row { position: relative; display: flex; align-items: center; gap: .625rem; min-height: 2rem; padding: .25rem .5rem; border-radius: .5rem; color: var(--comty-foreground); }
99
+ .comty-agent-row:hover { background: var(--comty-hover-surface); }
100
+ .comty-agent-dot { width: .4375rem; height: .4375rem; flex: 0 0 auto; border-radius: 50%; background: var(--comty-muted); }
101
+ .comty-working-indicator { display: inline-grid; grid-template-columns: repeat(3, var(--comty-working-dot-size)); gap: var(--comty-working-dot-gap); flex: 0 0 auto; color: var(--comty-foreground); }
102
+ .comty-working-indicator > i { display: block; width: var(--comty-working-dot-size); height: var(--comty-working-dot-size); border-radius: 50%; background: currentColor; opacity: .25; transition: opacity 150ms linear; }
103
+ .comty-working-indicator > i[data-active] { opacity: 1; }
104
+ .comty-agent-dot[data-status="success"] { background: var(--comty-agent-success); }
105
+ .comty-agent-dot[data-status="error"] { background: var(--comty-agent-error); }
106
+ .comty-agent-row-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
107
+ .comty-agent-row-state { flex: 0 0 auto; color: var(--comty-muted); font-size: .875rem; }
108
+ .comty-agent-row-actions { position: absolute; top: 50%; right: .25rem; display: flex; gap: .25rem; border-radius: .25rem; background: var(--comty-hover-surface); opacity: 0; pointer-events: none; transform: translateY(-50%); }
109
+ .comty-agent-row:hover .comty-agent-row-actions, .comty-agent-row-actions:has(.comty-agent-action:focus-visible) { opacity: 1; pointer-events: auto; }
110
+ @media (hover: none) { .comty-agent-row-actions { position: static; opacity: 1; pointer-events: auto; transform: none; background: transparent; } }
111
+ .comty-agent-panel .comty-agent-row-actions .comty-agent-action { min-height: 1.5rem; padding: .125rem .375rem; border-radius: .25rem; background: transparent; color: var(--comty-muted); font-size: .875rem; }
112
+ .comty-agent-panel .comty-agent-row-actions .comty-agent-action:hover { color: var(--comty-foreground); background: var(--comty-surface); }
113
+ .comty-agent-error-popover { position: fixed; z-index: 2147483647; width: min(20rem, calc(100vw - 1rem)); max-height: calc(100vh - 1rem); overflow: auto; padding: .75rem .875rem; border: 0; border-radius: .625rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .875rem/1.4 ui-sans-serif, system-ui, sans-serif; pointer-events: none; color-scheme: dark; }
114
+ .comty-agent-error-popover strong { display: block; font-weight: 600; }
115
+ .comty-agent-error-popover p { margin: .375rem 0 0; color: var(--comty-muted); white-space: pre-wrap; overflow-wrap: anywhere; }
116
+ .comty-agent-panel p { margin: .5rem 0 0; font: inherit; color: var(--comty-muted); }
117
+ .comty-agent-error { margin-top: .75rem; }
118
+ .comty-agent-error strong { display: block; font-weight: 500; color: var(--comty-foreground); }
119
+ .comty-agent-error p { margin-top: .25rem; }
120
+ .comty-agent-banner { margin-bottom: 1rem; }
121
+ .comty-agent-banner .comty-agent-error { margin-top: 0; }
122
+ .comty-agent-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .875rem; }
123
+ .comty-agent-panel .comty-agent-action { display: inline-flex; align-items: center; justify-content: center; gap: .375rem; min-height: 2rem; margin: 0; padding: .375rem .625rem; border: 0; border-radius: .5rem; font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; cursor: pointer; color: var(--comty-foreground); background: var(--comty-hover-surface); transition: background-color 150ms ease; }
124
+ .comty-agent-panel .comty-agent-action:hover { background: color-mix(in srgb, var(--comty-foreground) 12%, var(--comty-hover-surface)); }
125
+ .comty-agent-panel .comty-agent-action[data-primary] { color: var(--comty-paper); background: var(--comty-accent); }
126
+ .comty-agent-panel .comty-agent-action:disabled { opacity: .5; cursor: wait; }
127
+ .comty-agent-panel .comty-agent-close { width: 1.5rem; min-height: 1.5rem; padding: 0; background: transparent; color: var(--comty-muted); }
128
+ .comty-agent-panel :is(button, summary):focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .1875rem; }
129
+ .comty-agent-details { margin-top: .625rem; }
130
+ .comty-agent-panel summary { cursor: pointer; color: var(--comty-muted); font-size: .875rem; }
131
+ .comty-agent-panel pre { margin: .5rem 0 0; padding: .625rem; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 10rem; overflow: auto; border-radius: .5rem; background: var(--comty-surface); color: var(--comty-muted); font: .875rem/1.4 ui-monospace, monospace; }
132
+ .comty-agent-working { position: fixed; z-index: 1; box-sizing: border-box; border: 2px solid var(--comty-accent); border-radius: 0; pointer-events: none; }
133
+ .comty-agent-working-badge { position: absolute; left: -.125rem; bottom: 100%; display: inline-flex; align-items: center; gap: .4rem; padding: .25rem .5rem; border-radius: .375rem .375rem 0 0; background: var(--comty-surface); color: var(--comty-foreground); font: 600 .6875rem/1.4 ui-sans-serif, system-ui, sans-serif; white-space: nowrap; }
134
+ .comty-agent-working[data-badge-below] .comty-agent-working-badge { bottom: auto; top: 100%; border-radius: 0 0 .375rem .375rem; }
73
135
  #comty-root { cursor: default; }
74
136
  #comty-root::backdrop { background: transparent; pointer-events: none; }
75
137
  html.comty-comment-placement,
76
138
  html.comty-comment-placement body,
77
139
  html.comty-comment-placement body *:not(#comty-root):not(#comty-root *) {
78
- cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}") 20 20, crosshair !important;
140
+ cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}") 16 16, crosshair !important;
79
141
  }
80
142
  #comty-root > .comty-sidepanel-open, #comty-root > .comty-pin, #comty-root > .comty-bubble, #comty-root > .comty-create-bubble, #comty-root > .comty-popover, #comty-root > .comty-auth-backdrop, #comty-root > .comty-auth-toast { pointer-events: auto; }
81
143
  .comty-toolbar {
@@ -142,7 +204,7 @@ export function getComtyStyle() {
142
204
  #comty-root [hidden] { display: none !important; }
143
205
  [data-comty-anchor-hidden] { visibility: hidden !important; pointer-events: none !important; }
144
206
  @media (prefers-reduced-motion: reduce) {
145
- .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble { transition: none !important; animation: none !important; }
207
+ .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble, .comty-agent-panel, .comty-agent-action, .comty-agent-dot, .comty-working-indicator > i { transition: none !important; animation: none !important; }
146
208
  }
147
209
 
148
210
  .comty-sidepanel {
@@ -324,6 +386,16 @@ export function getComtyStyle() {
324
386
  white-space: nowrap;
325
387
  }
326
388
 
389
+ .comty-anchor-badge {
390
+ display: inline-block;
391
+ margin-top: .375rem;
392
+ padding: .125rem .375rem;
393
+ border: 1px solid var(--comty-border);
394
+ border-radius: 999rem;
395
+ color: var(--comty-muted);
396
+ font: .6875rem/1.4 sans-serif;
397
+ }
398
+
327
399
  .comty-pin {
328
400
  position: fixed;
329
401
  z-index: 2147483645;
@@ -605,6 +677,8 @@ export function getComtyStyle() {
605
677
  height: 100%;
606
678
  object-fit: cover;
607
679
  }
680
+ .comty-comment-avatar.comty-agent-avatar { overflow: visible; border-radius: 0; background: transparent; color: var(--comty-paper); }
681
+ .comty-comment-avatar.comty-agent-avatar svg { display: block; width: 100%; height: 100%; }
608
682
 
609
683
  .comty-pin-avatar {
610
684
  width: 24px;
@@ -719,12 +793,12 @@ export function getComtyStyle() {
719
793
  }
720
794
 
721
795
  .comty-popover-item.comty-popover-item-destructive {
722
- color: var(--comty-error);
796
+ color: var(--comty-agent-error);
723
797
  }
724
798
 
725
799
  .comty-popover-item.comty-popover-item-destructive:hover,
726
800
  .comty-popover-item.comty-popover-item-destructive:focus-visible {
727
- background: color-mix(in srgb, var(--comty-error) 12%, var(--comty-surface));
801
+ background: color-mix(in srgb, var(--comty-agent-error) 12%, var(--comty-surface));
728
802
  outline: none;
729
803
  }
730
804
 
@@ -1095,15 +1169,15 @@ export function getComtyStyle() {
1095
1169
  margin: 4px 0;
1096
1170
  }
1097
1171
 
1098
- html.comty-comments-active .comty-pin {
1172
+ html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin {
1099
1173
  display: grid;
1100
1174
  }
1101
1175
 
1102
- html.comty-comments-active .comty-pin.comty-filtered-out {
1176
+ html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin.comty-filtered-out {
1103
1177
  display: none;
1104
1178
  }
1105
1179
 
1106
- html.comty-comments-active .comty-bubble {
1180
+ html:is(.comty-comments-active, .comty-inspect-annotations) .comty-bubble {
1107
1181
  display: block;
1108
1182
  }
1109
1183
 
@@ -1213,10 +1287,10 @@ export function getComtyStyle() {
1213
1287
  position: fixed;
1214
1288
  box-sizing: border-box;
1215
1289
  border: .0625rem solid var(--comty-outline);
1216
- background: color-mix(in oklch, var(--comty-outline) 8%, transparent);
1290
+ background: transparent;
1291
+ border-radius: 0;
1217
1292
  pointer-events: none;
1218
1293
  }
1219
- .comty-inspection-box[data-selected] { background: color-mix(in oklch, var(--comty-outline) 5%, transparent); }
1220
1294
  .comty-inspect-spacing { position: fixed; pointer-events: none; background: var(--comty-spacing-color); }
1221
1295
  .comty-inspect-margin { --comty-spacing-color: var(--comty-margin-fill); }
1222
1296
  .comty-inspect-padding { --comty-spacing-color: var(--comty-padding-fill); }
@@ -1270,6 +1344,15 @@ export function getComtyStyle() {
1270
1344
  height: 5.75rem;
1271
1345
  border-radius: .75rem;
1272
1346
  }
1347
+ .comty-agent-scope { display: inline-flex; align-items: center; gap: .375rem; color: var(--comty-muted); font: .6875rem/1.25 var(--comty-font); white-space: nowrap; cursor: pointer; pointer-events: auto; opacity: 0; visibility: hidden; }
1348
+ .comty-create-expanded .comty-agent-scope { opacity: 1; visibility: visible; }
1349
+ .comty-agent-scope input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
1350
+ .comty-agent-scope-track { position: relative; width: 1.5rem; height: .875rem; flex: 0 0 auto; border-radius: 999rem; background: var(--comty-border); transition: background-color 160ms ease; }
1351
+ .comty-agent-scope-track::after { content: ""; position: absolute; top: .125rem; left: .125rem; width: .625rem; height: .625rem; border-radius: 50%; background: var(--comty-paper); transition: transform 160ms ease; }
1352
+ .comty-agent-scope input:checked + .comty-agent-scope-track { background: var(--comty-accent); }
1353
+ .comty-agent-scope input:checked + .comty-agent-scope-track::after { transform: translateX(.625rem); }
1354
+ .comty-agent-scope input:focus-visible + .comty-agent-scope-track { outline: .125rem solid var(--comty-comment-focus); outline-offset: .125rem; }
1355
+ .comty-agent-thread-error { grid-column: 2; margin: .25rem 0 0; color: var(--comty-agent-error); font: .6875rem/1.3 var(--comty-font); }
1273
1356
 
1274
1357
  .comty-create-bubble.comty-create-has-attachment { height: 7.5rem; }
1275
1358
  .comty-create-bubble.comty-bubble-left { transform-origin: top right; }
package/src/vite.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { ComtyOptions } from './browser.js';
3
+
4
+ /** Development only: leaves production builds untouched. */
5
+ export function comty(options?: Pick<ComtyOptions, 'projectKey' | 'apiUrl'> & { agent?: 'codex' | 'claude' | 'cursor' | 'gemini' | 'grok'; model?: string }): Plugin;
package/src/vite.mjs ADDED
@@ -0,0 +1,68 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createCompanion, isLoopback } from './companion.mjs';
3
+
4
+ /** Starts the local companion on Vite's own origin, without a second port. */
5
+ export function comty(options = {}) {
6
+ let companion, origin, start;
7
+ return {
8
+ name: 'comty', apply: 'serve',
9
+ configureServer(server) {
10
+ const host = server.config.server.host;
11
+ if (host === true || (typeof host === 'string' && !isLoopback(host))) {
12
+ throw new Error('Comty Agent requires Vite to bind localhost or 127.0.0.1');
13
+ }
14
+ if (!server.httpServer) throw new Error('Comty requires a Vite HTTP server (middleware mode is unsupported)');
15
+ const runtime = new URL('./browser.js', import.meta.url);
16
+ const style = new URL('./style.js', import.meta.url);
17
+ const anchors = new URL('./anchors.js', import.meta.url);
18
+ const capture = new URL('./capture.js', import.meta.url);
19
+ const captureEngine = new URL('./capture-engine.js', import.meta.url);
20
+ const captureResources = new URL('./capture-resources.js', import.meta.url);
21
+ const comments = new URL('./comments.js', import.meta.url);
22
+ const measurements = new URL('./measurements.js', import.meta.url);
23
+ start = new Promise((resolve, reject) => {
24
+ const onError = error => {
25
+ // Vite retries the next port after EADDRINUSE unless strictPort is set.
26
+ if (error.code !== 'EADDRINUSE' || server.config.server.strictPort) reject(error);
27
+ };
28
+ server.httpServer.on('error', onError);
29
+ server.httpServer.once('listening', () => {
30
+ server.httpServer.off('error', onError);
31
+ try {
32
+ const address = server.httpServer.address();
33
+ const hostname = typeof host === 'string' ? host : 'localhost';
34
+ origin = `${server.config.server.https ? 'https' : 'http'}://${hostname}:${address.port}`;
35
+ companion = createCompanion({ cwd: server.config.root, origin, provider: options.agent, model: options.model });
36
+ server.httpServer.once('close', () => { void companion.close().catch(error => server.config.logger.error(`Comty: ${error.message}`)); });
37
+ resolve();
38
+ } catch (error) { reject(error); }
39
+ });
40
+ });
41
+ start.catch(error => server.config.logger.error(`Comty: ${error.message}`));
42
+ server.middlewares.use(async (request, response, next) => {
43
+ if (!request.url?.startsWith('/__comty/')) return next();
44
+ try {
45
+ await start;
46
+ if (request.headers.host !== new URL(origin).host || (request.headers.origin && request.headers.origin !== origin)) {
47
+ response.writeHead(403); response.end(); return;
48
+ }
49
+ const path = new URL(request.url, origin).pathname;
50
+ if (request.method === 'GET' && ['/__comty/browser.js', '/__comty/style.js', '/__comty/anchors.js', '/__comty/capture.js', '/__comty/capture-engine.js', '/__comty/capture-resources.js', '/__comty/comments.js', '/__comty/measurements.js', '/__comty/start.js'].includes(path)) {
51
+ response.setHeader('Content-Type', 'text/javascript'); response.setHeader('Cache-Control', 'no-store');
52
+ response.setHeader('X-Content-Type-Options', 'nosniff');
53
+ response.end(path.endsWith('/browser.js') ? await readFile(runtime) : path.endsWith('/style.js') ? await readFile(style) : path.endsWith('/anchors.js') ? await readFile(anchors) : path.endsWith('/capture.js') ? await readFile(capture) : path.endsWith('/capture-engine.js') ? await readFile(captureEngine) : path.endsWith('/capture-resources.js') ? await readFile(captureResources) : path.endsWith('/comments.js') ? await readFile(comments) : path.endsWith('/measurements.js') ? await readFile(measurements) :
54
+ `import { initComty } from '/__comty/browser.js';\ninitComty(${JSON.stringify({ projectKey: options.projectKey, apiUrl: options.apiUrl, agent: { url: `${origin}/__comty`, token: companion.token } })}).catch(console.error);`);
55
+ } else await companion.handle(request, response);
56
+ } catch (error) { next(error); }
57
+ });
58
+ },
59
+ transformIndexHtml: {
60
+ order: 'pre',
61
+ async handler() {
62
+ await start;
63
+ return [{ tag: 'script', attrs: { type: 'module', src: '/__comty/start.js', 'vite-ignore': '' }, injectTo: 'head-prepend' }];
64
+ },
65
+ },
66
+ async closeBundle() { await companion?.close(); },
67
+ };
68
+ }