@linkright.in/agent 1.0.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/package.json +22 -0
- package/src/index.js +237 -0
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@linkright.in/agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "LinkRight local agent — runs AI CLIs on your machine, connects to linkright.in",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"linkright-agent": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js",
|
|
11
|
+
"build": "npx esbuild src/index.js --bundle --platform=node --outfile=dist/linkright-agent.js --format=cjs"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"ws": "^8.0.0"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* LinkRight Agent — Local AI Terminal Bridge
|
|
4
|
+
*
|
|
5
|
+
* Runs on the user's machine. Exposes ws://localhost:7777
|
|
6
|
+
* LinkRight UI connects to this and sends prompts.
|
|
7
|
+
* Agent runs AI CLIs locally and streams output back.
|
|
8
|
+
*
|
|
9
|
+
* Zero API cost for Satvik — all AI calls use user's local tools.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* npx linkright-agent # auto-detects best available CLI
|
|
13
|
+
* npx linkright-agent --port 7777
|
|
14
|
+
* npx linkright-agent --cli opencode
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { WebSocketServer, WebSocket } = require('ws');
|
|
18
|
+
const { spawn, execSync } = require('child_process');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const PORT = parseInt(process.argv.find(a => a.startsWith('--port='))?.split('=')[1] || '7777');
|
|
23
|
+
const PREFERRED_CLI = process.argv.find(a => a.startsWith('--cli='))?.split('=')[1] || null;
|
|
24
|
+
|
|
25
|
+
// ── CLI Detection ─────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const CLI_OPTIONS = [
|
|
28
|
+
{
|
|
29
|
+
name: 'opencode',
|
|
30
|
+
check: () => which('opencode'),
|
|
31
|
+
install: 'npm install -g @opencode-ai/cli',
|
|
32
|
+
run: (prompt) => ['opencode', ['--print', prompt]],
|
|
33
|
+
streaming: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'gemini',
|
|
37
|
+
check: () => which('gemini'),
|
|
38
|
+
install: 'npm install -g @google/gemini-cli',
|
|
39
|
+
run: (prompt) => ['gemini', [prompt]],
|
|
40
|
+
streaming: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'claude',
|
|
44
|
+
check: () => which('claude'),
|
|
45
|
+
install: 'npm install -g @anthropic-ai/claude-code',
|
|
46
|
+
run: (prompt) => ['claude', ['--print', prompt]],
|
|
47
|
+
streaming: true,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function which(cmd) {
|
|
52
|
+
try {
|
|
53
|
+
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
// Try common install paths
|
|
57
|
+
const paths = [
|
|
58
|
+
path.join(os.homedir(), '.npm-global', 'bin', cmd),
|
|
59
|
+
path.join(os.homedir(), '.local', 'bin', cmd),
|
|
60
|
+
`/usr/local/bin/${cmd}`,
|
|
61
|
+
];
|
|
62
|
+
return paths.some(p => {
|
|
63
|
+
try { execSync(`test -f ${p}`, { stdio: 'ignore' }); return true; } catch { return false; }
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function detectCli() {
|
|
69
|
+
if (PREFERRED_CLI) {
|
|
70
|
+
const cli = CLI_OPTIONS.find(c => c.name === PREFERRED_CLI);
|
|
71
|
+
if (cli && cli.check()) return cli;
|
|
72
|
+
console.log(`⚠️ Preferred CLI '${PREFERRED_CLI}' not found. Auto-detecting...`);
|
|
73
|
+
}
|
|
74
|
+
return CLI_OPTIONS.find(c => c.check()) || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getCliPath(name) {
|
|
78
|
+
const paths = [
|
|
79
|
+
name,
|
|
80
|
+
path.join(os.homedir(), '.npm-global', 'bin', name),
|
|
81
|
+
path.join(os.homedir(), '.local', 'bin', name),
|
|
82
|
+
`/usr/local/bin/${name}`,
|
|
83
|
+
];
|
|
84
|
+
for (const p of paths) {
|
|
85
|
+
try { execSync(`test -f ${p} || which ${name}`, { stdio: 'ignore' }); return name; } catch {}
|
|
86
|
+
}
|
|
87
|
+
return name;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Agent info ────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function getAgentInfo() {
|
|
93
|
+
const available = CLI_OPTIONS.filter(c => c.check()).map(c => c.name);
|
|
94
|
+
const active = detectCli();
|
|
95
|
+
return {
|
|
96
|
+
type: 'agent_info',
|
|
97
|
+
version: '1.0.0',
|
|
98
|
+
platform: `${os.type()} ${os.arch()}`,
|
|
99
|
+
available_clis: available,
|
|
100
|
+
active_cli: active?.name || null,
|
|
101
|
+
ready: !!active,
|
|
102
|
+
install_hint: active ? null : `Install a CLI: ${CLI_OPTIONS.map(c => c.install).join(' OR ')}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── WebSocket Server ──────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
const wss = new WebSocketServer({
|
|
109
|
+
port: PORT,
|
|
110
|
+
// Allow connections from linkright.in and localhost
|
|
111
|
+
verifyClient: ({ origin }) => {
|
|
112
|
+
const allowed = [
|
|
113
|
+
'https://linkright.in',
|
|
114
|
+
'https://www.linkright.in',
|
|
115
|
+
'http://localhost:3000',
|
|
116
|
+
'http://localhost:5173',
|
|
117
|
+
];
|
|
118
|
+
if (!origin || allowed.some(o => origin.startsWith(o))) return true;
|
|
119
|
+
console.log(`⚠️ Rejected origin: ${origin}`);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const activeProcs = new Map(); // id → child process
|
|
125
|
+
|
|
126
|
+
wss.on('connection', (ws, req) => {
|
|
127
|
+
console.log(`🔗 Connected from ${req.headers.origin || 'unknown'}`);
|
|
128
|
+
|
|
129
|
+
// Send agent info immediately
|
|
130
|
+
ws.send(JSON.stringify(getAgentInfo()));
|
|
131
|
+
|
|
132
|
+
ws.on('message', async (raw) => {
|
|
133
|
+
let msg;
|
|
134
|
+
try { msg = JSON.parse(raw); } catch { return; }
|
|
135
|
+
|
|
136
|
+
if (msg.type === 'ping') {
|
|
137
|
+
ws.send(JSON.stringify({ type: 'pong', id: msg.id }));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (msg.type === 'info') {
|
|
142
|
+
ws.send(JSON.stringify(getAgentInfo()));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (msg.type === 'run') {
|
|
147
|
+
const { id, prompt, cli: preferredCli } = msg;
|
|
148
|
+
|
|
149
|
+
// Pick CLI
|
|
150
|
+
const selectedCli = preferredCli
|
|
151
|
+
? CLI_OPTIONS.find(c => c.name === preferredCli && c.check())
|
|
152
|
+
: detectCli();
|
|
153
|
+
|
|
154
|
+
if (!selectedCli) {
|
|
155
|
+
ws.send(JSON.stringify({
|
|
156
|
+
type: 'error',
|
|
157
|
+
id,
|
|
158
|
+
error: `No AI CLI found. Install one:\n${CLI_OPTIONS.map(c => ` ${c.install}`).join('\n')}`,
|
|
159
|
+
}));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`▶ Running ${selectedCli.name}: ${prompt.substring(0, 60)}...`);
|
|
164
|
+
|
|
165
|
+
const [cmd, args] = selectedCli.run(prompt);
|
|
166
|
+
const cliPath = getCliPath(cmd);
|
|
167
|
+
|
|
168
|
+
const proc = spawn(cliPath, args, {
|
|
169
|
+
env: { ...process.env },
|
|
170
|
+
shell: false,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
activeProcs.set(id, proc);
|
|
174
|
+
ws.send(JSON.stringify({ type: 'started', id, cli: selectedCli.name }));
|
|
175
|
+
|
|
176
|
+
let outputBuffer = '';
|
|
177
|
+
|
|
178
|
+
proc.stdout.on('data', (chunk) => {
|
|
179
|
+
outputBuffer += chunk.toString();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
proc.stderr.on('data', (chunk) => {
|
|
183
|
+
// Silently discard stderr (MCP errors, warnings etc.)
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
proc.on('close', (code) => {
|
|
187
|
+
activeProcs.delete(id);
|
|
188
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
189
|
+
// Send clean final response only
|
|
190
|
+
const cleanOutput = outputBuffer.trim();
|
|
191
|
+
ws.send(JSON.stringify({ type: 'result', id, code, data: cleanOutput }));
|
|
192
|
+
}
|
|
193
|
+
console.log(`✓ ${selectedCli.name} exited (code ${code})`);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
proc.on('error', (err) => {
|
|
197
|
+
activeProcs.delete(id);
|
|
198
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
199
|
+
ws.send(JSON.stringify({ type: 'error', id, error: err.message }));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (msg.type === 'kill') {
|
|
205
|
+
const proc = activeProcs.get(msg.id);
|
|
206
|
+
if (proc) {
|
|
207
|
+
proc.kill();
|
|
208
|
+
activeProcs.delete(msg.id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
ws.on('close', () => {
|
|
214
|
+
console.log('🔌 Disconnected');
|
|
215
|
+
// Kill all processes for this connection
|
|
216
|
+
activeProcs.forEach((proc) => proc.kill());
|
|
217
|
+
activeProcs.clear();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ── Startup ───────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
const info = getAgentInfo();
|
|
224
|
+
|
|
225
|
+
console.log('\n╔════════════════════════════════════════╗');
|
|
226
|
+
console.log('║ LinkRight Agent v1.0.0 ║');
|
|
227
|
+
console.log('╚════════════════════════════════════════╝\n');
|
|
228
|
+
console.log(`🖥️ Platform: ${info.platform}`);
|
|
229
|
+
console.log(`🔍 Available CLIs: ${info.available_clis.join(', ') || 'none'}`);
|
|
230
|
+
console.log(`✅ Active CLI: ${info.active_cli || 'none'}`);
|
|
231
|
+
if (!info.ready) {
|
|
232
|
+
console.log(`\n⚠️ No AI CLI found! Install one:`);
|
|
233
|
+
CLI_OPTIONS.forEach(c => console.log(` ${c.install}`));
|
|
234
|
+
}
|
|
235
|
+
console.log(`\n🌐 Listening on ws://localhost:${PORT}`);
|
|
236
|
+
console.log(`📡 Open linkright.in/sync → AI Terminal\n`);
|
|
237
|
+
console.log('Press Ctrl+C to stop\n');
|