@comty.io/core 0.1.2 → 0.1.3

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/companion.mjs DELETED
@@ -1,207 +0,0 @@
1
- import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
- import { mkdirSync, openSync, closeSync, readFileSync, writeFileSync, renameSync, unlinkSync, fsyncSync, realpathSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- import { runCodex } from './codex.mjs';
5
- import { runClaude } from './claude.mjs';
6
- import { runCursor } from './cursor.mjs';
7
-
8
- const statuses = new Set(['todo', 'in_progress', 'in_review', 'done', 'cancel']);
9
- export const isLoopback = hostname => ['localhost', '127.0.0.1', '[::1]'].includes(hostname);
10
-
11
- function httpError(status, message) { return Object.assign(new Error(message), { status }); }
12
- async function body(request) {
13
- const chunks = [];
14
- let size = 0;
15
- for await (const chunk of request) {
16
- size += chunk.length;
17
- if (size > 65536) throw httpError(413, 'Request exceeds 64 KiB');
18
- chunks.push(chunk);
19
- }
20
- try { return JSON.parse(Buffer.concat(chunks).toString('utf8')); }
21
- catch { throw httpError(400, 'Invalid JSON'); }
22
- }
23
-
24
- function newTask(input, host) {
25
- if (!input || typeof input !== 'object' || Array.isArray(input)) throw httpError(400, 'Invalid task');
26
- const fields = { content: 10000, selector: 4000, pagePath: 4000 };
27
- for (const [key, limit] of Object.entries(fields)) {
28
- if (typeof input[key] !== 'string' || !input[key].trim() || input[key].length > limit) throw httpError(400, `Invalid ${key}`);
29
- }
30
- if (!input.pagePath.startsWith('/')) throw httpError(400, 'Invalid pagePath');
31
- for (const key of ['relX', 'relY']) {
32
- if (typeof input[key] !== 'number' || !Number.isFinite(input[key]) || input[key] < 0 || input[key] > 1) throw httpError(400, `Invalid ${key}`);
33
- }
34
- for (const [key, limit] of Object.entries({ domElement: 8000, viewport: 100, anchorContext: 24000 })) {
35
- if (input[key] != null && (typeof input[key] !== 'string' || input[key].length > limit)) throw httpError(400, `Invalid ${key}`);
36
- }
37
- if (input.anchorContext) {
38
- try { JSON.parse(input.anchorContext); } catch { throw httpError(400, 'Invalid anchorContext JSON'); }
39
- }
40
- return {
41
- id: randomUUID(), kind: 'agent', local_agent: true, status: 'todo',
42
- domain: host, page_path: input.pagePath, selector: input.selector,
43
- rel_x: input.relX, rel_y: input.relY, content: input.content.trim(),
44
- dom_element: input.domElement || null, viewport: input.viewport || null,
45
- anchor_context: input.anchorContext || null, author_name: 'You',
46
- created_at: new Date().toISOString(), replies: [],
47
- };
48
- }
49
-
50
- /** Internal local server shared by the Vite integration and the dev command. */
51
- export function createCompanion({ cwd, origin, provider = 'codex', model, runAgent }) {
52
- if (!['codex', 'claude', 'cursor'].includes(provider)) throw new Error('Supported agents: codex, claude, cursor');
53
- if (model !== undefined && (provider !== 'codex' || typeof model !== 'string' || !model.trim() || model.length > 100)) throw new Error('model must be a Codex model ID');
54
- runAgent ||= { codex: runCodex, claude: runClaude, cursor: runCursor }[provider];
55
- const site = new URL(origin);
56
- if (!isLoopback(site.hostname) || !['http:', 'https:'].includes(site.protocol)) throw new Error('Comty agent requires a loopback origin');
57
- cwd = realpathSync(cwd);
58
- const directory = resolve(cwd, '.comty');
59
- const file = resolve(directory, 'tasks.json');
60
- const lock = resolve(directory, 'companion.lock');
61
- mkdirSync(directory, { recursive: true, mode: 0o700 });
62
- let descriptor;
63
- try { descriptor = openSync(lock, 'wx', 0o600); }
64
- catch (error) {
65
- if (error.code !== 'EEXIST') throw error;
66
- throw new Error(`Comty workspace is locked: ${lock}. Stop its companion first. After a crash, verify the recorded PID is no longer running before removing this lock.`);
67
- }
68
- writeFileSync(descriptor, JSON.stringify({ pid: process.pid, origin }));
69
- closeSync(descriptor);
70
- let tasks;
71
- function persist() {
72
- const temporary = `${file}.tmp`;
73
- const fd = openSync(temporary, 'w', 0o600);
74
- try { writeFileSync(fd, `${JSON.stringify(tasks, null, 2)}\n`); fsyncSync(fd); }
75
- finally { closeSync(fd); }
76
- renameSync(temporary, file);
77
- }
78
- try {
79
- try { tasks = JSON.parse(readFileSync(file, 'utf8')); }
80
- catch (error) { if (error.code === 'ENOENT') tasks = []; else throw error; }
81
- if (!Array.isArray(tasks) || tasks.some(task => !task?.id || !statuses.has(task.status))) throw new Error('Invalid Comty task history; refusing to overwrite it');
82
- for (const task of tasks) {
83
- if (['todo', 'in_progress'].includes(task.status)) {
84
- task.status = 'in_review'; task.error = 'Development server stopped. Review any changes before retrying.';
85
- }
86
- }
87
- persist();
88
- } catch (error) { unlinkSync(lock); throw error; }
89
- const releaseLock = () => {
90
- try { unlinkSync(lock); } catch (error) { if (error.code !== 'ENOENT') throw error; }
91
- };
92
- const onInterrupt = () => { releaseLock(); process.exit(130); };
93
- const onTerminate = () => { releaseLock(); process.exit(143); };
94
- process.once('exit', releaseLock);
95
- process.once('SIGINT', onInterrupt);
96
- process.once('SIGTERM', onTerminate);
97
- const token = randomBytes(32).toString('hex');
98
- const approvals = new Map();
99
- const active = new Map();
100
- let closing = false, closed;
101
- function update(task, values) {
102
- Object.assign(task, values, { updated_at: new Date().toISOString() });
103
- persist();
104
- }
105
- function start(task) {
106
- if (closing || active.has(task.id) || task.status !== 'todo') return;
107
- const controller = new AbortController();
108
- const running = (async () => {
109
- const timeout = setTimeout(() => controller.abort(new Error('Agent exceeded 30 minutes; review changes before retrying.')), 30 * 60 * 1000);
110
- try {
111
- update(task, { status: 'in_progress', error: null });
112
- const receipt = await runAgent({
113
- cwd, task, signal: controller.signal, model,
114
- progress: message => { task.progress = String(message).slice(-4000); },
115
- approve: request => new Promise(resolve => {
116
- if (controller.signal.aborted) return resolve(false);
117
- const id = randomUUID();
118
- const abort = () => { approvals.delete(id); resolve(false); };
119
- controller.signal.addEventListener('abort', abort, { once: true });
120
- approvals.set(id, { id, taskId: task.id, ...request, resolve: accepted => {
121
- controller.signal.removeEventListener('abort', abort); approvals.delete(id); resolve(accepted);
122
- } });
123
- }),
124
- });
125
- controller.signal.throwIfAborted();
126
- if (!receipt || !['done', 'blocked'].includes(receipt.status) || !receipt.summary || !receipt.verification) throw new Error('Agent completion was not verified');
127
- update(task, { status: receipt.status === 'done' ? 'done' : 'in_review', result: receipt });
128
- } catch (error) {
129
- update(task, { status: task.status === 'cancel' ? 'cancel' : 'in_review', error: error.message || String(error) });
130
- } finally {
131
- clearTimeout(timeout);
132
- for (const approval of [...approvals.values()]) if (approval.taskId === task.id) approval.resolve(false);
133
- active.delete(task.id);
134
- }
135
- })();
136
- active.set(task.id, { controller, running });
137
- // A storage failure must stop new work rather than silently lose history.
138
- void running.catch(error => { closing = true; console.error('Comty stopped local agents:', error); });
139
- }
140
- function snapshot() {
141
- return { provider, tasks, approvals: [...approvals.values()].map(({ resolve: _, ...approval }) => approval) };
142
- }
143
- async function handle(request, response) {
144
- const json = (status, value) => {
145
- response.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
146
- response.end(JSON.stringify(value));
147
- };
148
- try {
149
- if (closing) throw httpError(503, 'Companion is shutting down');
150
- const supplied = Buffer.from(String(request.headers['x-comty-token'] || ''));
151
- const expected = Buffer.from(token);
152
- if (request.headers.host !== site.host || (request.headers.origin && request.headers.origin !== site.origin) ||
153
- supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw httpError(403, 'Invalid local session');
154
- const url = new URL(request.url, site);
155
- if (request.method === 'GET' && url.pathname === '/__comty/tasks') return json(200, snapshot());
156
- if (request.method === 'POST' && url.pathname === '/__comty/tasks') {
157
- const task = newTask(await body(request), site.host);
158
- tasks.push(task);
159
- try { persist(); } catch (error) { tasks.pop(); throw error; }
160
- json(201, { task }); start(task); return;
161
- }
162
- const action = url.pathname.match(/^\/__comty\/tasks\/([a-f0-9-]+)\/(retry|cancel)$/);
163
- if (request.method === 'POST' && action) {
164
- const task = tasks.find(item => item.id === action[1]);
165
- if (!task) throw httpError(404, 'Unknown task');
166
- if (action[2] === 'retry') {
167
- if (!['in_review', 'cancel'].includes(task.status)) throw httpError(409, 'Task cannot be retried in this state');
168
- if (active.has(task.id)) throw httpError(409, 'Agent is still stopping');
169
- update(task, { status: 'todo', error: null, progress: null, result: null }); start(task);
170
- } else {
171
- if (task.status === 'done') throw httpError(409, 'Task is already done');
172
- update(task, { status: 'cancel' });
173
- active.get(task.id)?.controller.abort(new Error('Cancelled by user; review any partial changes.'));
174
- }
175
- return json(200, { task });
176
- }
177
- const approvalPath = url.pathname.match(/^\/__comty\/approvals\/([a-f0-9-]+)$/);
178
- if (request.method === 'POST' && approvalPath) {
179
- const approval = approvals.get(approvalPath[1]);
180
- if (!approval) throw httpError(409, 'Approval expired');
181
- const input = await body(request);
182
- if (typeof input?.allow !== 'boolean') throw httpError(400, 'Expected allow: boolean');
183
- approval.resolve(input.allow);
184
- return json(200, { ok: true });
185
- }
186
- throw httpError(404, 'Not found');
187
- } catch (error) {
188
- json(error.status || 500, { error: error.status ? error.message : 'Local task storage failed. Check the terminal.' });
189
- if (!error.status) console.error(error);
190
- }
191
- }
192
- return {
193
- token, handle,
194
- close() {
195
- if (closed) return closed;
196
- closing = true;
197
- for (const entry of active.values()) entry.controller.abort(new Error('Development server stopped'));
198
- closed = Promise.allSettled([...active.values()].map(entry => entry.running)).then(() => {
199
- process.removeListener('exit', releaseLock);
200
- process.removeListener('SIGINT', onInterrupt);
201
- process.removeListener('SIGTERM', onTerminate);
202
- releaseLock();
203
- });
204
- return closed;
205
- },
206
- };
207
- }
package/src/cursor.mjs DELETED
@@ -1,49 +0,0 @@
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, executable = 'agent', args = [] }) {
6
- cwd = realpathSync(cwd);
7
- let output = '';
8
- const rpc = openAgentProcess({
9
- executable, args: [...args, '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 DELETED
@@ -1,169 +0,0 @@
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 comments = await readFile(new URL('./comments.js', import.meta.url));
22
- const measurements = await readFile(new URL('./measurements.js', import.meta.url));
23
- let companion, origin, child, closing, finished;
24
- const sockets = new Set();
25
- const proxies = new Set();
26
- const server = createServer((request, response) => {
27
- if (!origin || request.headers.host !== new URL(origin).host) {
28
- response.writeHead(403); response.end('Comty is available on its loopback URL only'); return;
29
- }
30
- let url;
31
- try { url = new URL(request.url, origin); }
32
- catch { response.writeHead(400); response.end('Invalid request target'); return; }
33
- if (url.origin !== origin) { response.writeHead(400); response.end('Invalid request target'); return; }
34
- if (url.pathname.startsWith('/__comty/')) {
35
- if (request.headers.origin && request.headers.origin !== origin) {
36
- response.writeHead(403); response.end(); return;
37
- }
38
- if (request.method === 'GET' && ['/__comty/browser.js', '/__comty/style.js', '/__comty/anchors.js', '/__comty/capture.js', '/__comty/comments.js', '/__comty/measurements.js', '/__comty/start.js'].includes(url.pathname)) {
39
- response.writeHead(200, { 'Content-Type': 'text/javascript', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
40
- 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('/comments.js') ? comments : url.pathname.endsWith('/measurements.js') ? measurements :
41
- `import { initComty } from '/__comty/browser.js';\ninitComty(${JSON.stringify({ projectKey, apiUrl, agent: { url: `${origin}/__comty`, token: companion.token } })}).catch(console.error);`);
42
- return;
43
- }
44
- void companion.handle(request, response); return;
45
- }
46
- const destination = new URL(upstream);
47
- destination.pathname = url.pathname; destination.search = url.search;
48
- const proxy = httpRequest(destination, {
49
- method: request.method,
50
- headers: { ...request.headers, host: upstream.host, 'accept-encoding': 'identity' },
51
- }, incoming => {
52
- const headers = { ...incoming.headers };
53
- if (headers.location?.startsWith(upstream.origin)) headers.location = headers.location.replace(upstream.origin, origin);
54
- if (!headers['content-type']?.includes('text/html') || request.method === 'HEAD') {
55
- response.writeHead(incoming.statusCode, headers); incoming.pipe(response); return;
56
- }
57
- // ponytail: buffer development HTML for injection; use the Vite integration for streamed HTML.
58
- const chunks = []; let length = 0;
59
- incoming.on('data', chunk => {
60
- length += chunk.length;
61
- if (length > 8 * 1024 * 1024) incoming.destroy(new Error('HTML exceeds the 8 MiB development limit'));
62
- else chunks.push(chunk);
63
- });
64
- incoming.on('error', error => { if (!response.headersSent) response.writeHead(502); response.end(error.message); });
65
- incoming.on('end', () => {
66
- if (incoming.headers['content-encoding'] && incoming.headers['content-encoding'] !== 'identity') {
67
- response.writeHead(502); response.end('Comty requires uncompressed development HTML'); return;
68
- }
69
- const html = Buffer.concat(chunks).toString('utf8');
70
- const script = '<script type="module" src="/__comty/start.js"></script>';
71
- const injected = /<head(?:\s[^>]*)?>/i.test(html) ? html.replace(/<head(?:\s[^>]*)?>/i, match => match + script) : script + html;
72
- delete headers['content-length']; delete headers.etag; delete headers['content-encoding'];
73
- headers['cache-control'] = 'no-store';
74
- response.writeHead(incoming.statusCode, headers); response.end(injected);
75
- });
76
- });
77
- proxies.add(proxy); proxy.on('close', () => proxies.delete(proxy));
78
- proxy.on('error', () => {
79
- if (!response.headersSent) response.writeHead(502, { 'Content-Type': 'text/plain' });
80
- response.end(`Development server unavailable at ${upstream.origin}. Check its terminal output and reload.`);
81
- });
82
- request.on('aborted', () => proxy.destroy());
83
- response.on('close', () => proxy.destroy());
84
- request.pipe(proxy);
85
- });
86
- server.on('connection', socket => { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); });
87
- server.on('upgrade', (request, socket, head) => {
88
- if (request.headers.host !== new URL(origin).host || (request.headers.origin && request.headers.origin !== origin)) return socket.destroy();
89
- let url;
90
- try { url = new URL(request.url, origin); } catch { return socket.destroy(); }
91
- if (url.origin !== origin || url.pathname.startsWith('/__comty/')) return socket.destroy();
92
- const destination = new URL(upstream);
93
- destination.pathname = url.pathname; destination.search = url.search;
94
- const proxy = httpRequest(destination, { headers: { ...request.headers, host: upstream.host } });
95
- proxies.add(proxy); proxy.on('close', () => proxies.delete(proxy));
96
- proxy.on('upgrade', (response, upstreamSocket, upstreamHead) => {
97
- sockets.add(upstreamSocket); upstreamSocket.on('close', () => sockets.delete(upstreamSocket));
98
- 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`);
99
- if (upstreamHead.length) socket.write(upstreamHead);
100
- if (head.length) upstreamSocket.write(head);
101
- socket.pipe(upstreamSocket).pipe(socket);
102
- socket.on('error', () => upstreamSocket.destroy());
103
- upstreamSocket.on('error', () => socket.destroy());
104
- socket.on('close', () => upstreamSocket.destroy());
105
- upstreamSocket.on('close', () => socket.destroy());
106
- });
107
- proxy.on('response', response => { response.resume(); socket.destroy(); });
108
- proxy.on('error', () => socket.destroy());
109
- proxy.end();
110
- });
111
- await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, '127.0.0.1', resolve); });
112
- origin = `http://127.0.0.1:${server.address().port}`;
113
- if (new URL(origin).port === upstream.port) { server.close(); throw new Error('Comty and the application must use different ports'); }
114
- try { companion = createCompanion({ cwd, origin, provider, model }); }
115
- catch (error) { server.close(); throw error; }
116
- async function close() {
117
- if (closing) return closing;
118
- closing = (async () => {
119
- const kill = signal => {
120
- if (!child?.pid) return;
121
- try {
122
- if (process.platform === 'win32') child.kill(signal);
123
- else process.kill(-child.pid, signal);
124
- } catch (error) { if (error.code !== 'ESRCH') throw error; }
125
- };
126
- kill('SIGTERM');
127
- const force = setTimeout(() => kill('SIGKILL'), 2000);
128
- for (const proxy of proxies) proxy.destroy();
129
- for (const socket of sockets) socket.destroy();
130
- await Promise.all([companion.close(), new Promise(resolve => server.close(resolve)),
131
- child && child.exitCode === null && child.signalCode === null ? new Promise(resolve => child.once('close', resolve)) : Promise.resolve()]);
132
- clearTimeout(force);
133
- })();
134
- return closing;
135
- }
136
- if (command.length) {
137
- child = spawn(command[0], command.slice(1), {
138
- cwd, stdio: 'inherit', detached: process.platform !== 'win32', env: { ...process.env, PATH: `${resolve(cwd, 'node_modules/.bin')}${delimiter}${process.env.PATH || ''}` },
139
- });
140
- child.on('error', error => { console.error(`Comty could not start ${command[0]}: ${error.message}`); });
141
- finished = new Promise(resolve => child.once('close', code => resolve(Number.isInteger(code) && code >= 0 ? code : 1))).then(async code => {
142
- await close();
143
- return code;
144
- });
145
- finished.catch(error => console.error(`Comty shutdown failed: ${error.message}`));
146
- }
147
- return { origin, close, finished };
148
- }
149
-
150
- async function main() {
151
- const args = process.argv.slice(2);
152
- if (!args.length || args.includes('--help')) {
153
- console.log('Usage: comty dev [--agent codex|claude|cursor] [--model MODEL] [--target http://127.0.0.1:5173] [--port 4387] [--project-key KEY --api-url URL] -- vite --strictPort'); return;
154
- }
155
- if (args.shift() !== 'dev') throw new Error('Expected comty dev. Use --help for usage.');
156
- const split = args.indexOf('--');
157
- const { values } = parseArgs({ args: split < 0 ? args : args.slice(0, split), options: {
158
- target: { type: 'string' }, port: { type: 'string' }, agent: { type: 'string' }, model: { type: 'string' }, 'project-key': { type: 'string' }, 'api-url': { type: 'string' },
159
- } });
160
- const session = await startDev({ target: values.target, port: values.port === undefined ? 4387 : Number(values.port), provider: values.agent, model: values.model,
161
- projectKey: values['project-key'], apiUrl: values['api-url'], command: split < 0 ? [] : args.slice(split + 1) });
162
- console.log(`Comty: ${session.origin} — open this URL to annotate. Local tasks: .comty/tasks.json`);
163
- for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => { void session.close().catch(console.error); });
164
- if (session.finished) process.exitCode = await session.finished;
165
- }
166
-
167
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
168
- main().catch(error => { console.error(`Comty: ${error.message}`); process.exitCode = 1; });
169
- }
package/src/vite.d.ts DELETED
@@ -1,5 +0,0 @@
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'; model?: string }): Plugin;
package/src/vite.mjs DELETED
@@ -1,66 +0,0 @@
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 comments = new URL('./comments.js', import.meta.url);
20
- const measurements = new URL('./measurements.js', import.meta.url);
21
- start = new Promise((resolve, reject) => {
22
- const onError = error => {
23
- // Vite retries the next port after EADDRINUSE unless strictPort is set.
24
- if (error.code !== 'EADDRINUSE' || server.config.server.strictPort) reject(error);
25
- };
26
- server.httpServer.on('error', onError);
27
- server.httpServer.once('listening', () => {
28
- server.httpServer.off('error', onError);
29
- try {
30
- const address = server.httpServer.address();
31
- const hostname = typeof host === 'string' ? host : 'localhost';
32
- origin = `${server.config.server.https ? 'https' : 'http'}://${hostname}:${address.port}`;
33
- companion = createCompanion({ cwd: server.config.root, origin, provider: options.agent, model: options.model });
34
- server.httpServer.once('close', () => { void companion.close().catch(error => server.config.logger.error(`Comty: ${error.message}`)); });
35
- resolve();
36
- } catch (error) { reject(error); }
37
- });
38
- });
39
- start.catch(error => server.config.logger.error(`Comty: ${error.message}`));
40
- server.middlewares.use(async (request, response, next) => {
41
- if (!request.url?.startsWith('/__comty/')) return next();
42
- try {
43
- await start;
44
- if (request.headers.host !== new URL(origin).host || (request.headers.origin && request.headers.origin !== origin)) {
45
- response.writeHead(403); response.end(); return;
46
- }
47
- const path = new URL(request.url, origin).pathname;
48
- if (request.method === 'GET' && ['/__comty/browser.js', '/__comty/style.js', '/__comty/anchors.js', '/__comty/capture.js', '/__comty/comments.js', '/__comty/measurements.js', '/__comty/start.js'].includes(path)) {
49
- response.setHeader('Content-Type', 'text/javascript'); response.setHeader('Cache-Control', 'no-store');
50
- response.setHeader('X-Content-Type-Options', 'nosniff');
51
- 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('/comments.js') ? await readFile(comments) : path.endsWith('/measurements.js') ? await readFile(measurements) :
52
- `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);`);
53
- } else await companion.handle(request, response);
54
- } catch (error) { next(error); }
55
- });
56
- },
57
- transformIndexHtml: {
58
- order: 'pre',
59
- async handler() {
60
- await start;
61
- return [{ tag: 'script', attrs: { type: 'module', src: '/__comty/start.js', 'vite-ignore': '' }, injectTo: 'head-prepend' }];
62
- },
63
- },
64
- async closeBundle() { await companion?.close(); },
65
- };
66
- }