lattice-install 0.1.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.
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const readline = require('readline');
8
+ const https = require('https');
9
+ const { execSync } = require('child_process');
10
+
11
+ // ─── Constants ───────────────────────────────────────────────────────────────
12
+
13
+ const LATTICE_BASE_URL = 'https://latticeproxy.io';
14
+ const LATTICE_PROXY_URL = `${LATTICE_BASE_URL}/v1`;
15
+ const REPORT_URL = `${LATTICE_BASE_URL}/install/report`;
16
+ const DOCS_URL = `${LATTICE_BASE_URL}/docs`;
17
+
18
+ const HOME = os.homedir();
19
+
20
+ const PATHS = {
21
+ openclaw: path.join(HOME, '.openclaw', 'openclaw.json'),
22
+ continue: path.join(HOME, '.continue', 'config.json'),
23
+ aider: path.join(HOME, '.config', 'aider', 'config.yml'),
24
+ cursor: path.join(HOME, '.cursor', 'config.json'),
25
+ envFile: path.join(process.cwd(), '.env'),
26
+ bashrc: path.join(HOME, '.bashrc'),
27
+ zshrc: path.join(HOME, '.zshrc'),
28
+ };
29
+
30
+ // ─── Help ─────────────────────────────────────────────────────────────────────
31
+
32
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
33
+ console.log(`
34
+ lattice-install — Onboard to Lattice Network in 60 seconds
35
+
36
+ USAGE
37
+ lattice-install [options]
38
+
39
+ OPTIONS
40
+ --help, -h Show this help message
41
+
42
+ WHAT IT DOES
43
+ 1. Detects installed AI coding tools (OpenClaw, Continue, Cursor, Aider)
44
+ 2. Asks which AI provider you use (Anthropic default)
45
+ 3. Collects your API key
46
+ 4. Configures OpenClaw to route through Lattice proxy
47
+ 5. Adds ANTHROPIC_BASE_URL / OPENAI_BASE_URL to your shell profile
48
+ 6. Tests the connection
49
+ 7. Reports telemetry to latticeproxy.io (fire-and-forget)
50
+
51
+ SUPPORTED TOOLS
52
+ ✓ OpenClaw — config written automatically
53
+ ✓ Shell profile (.bashrc / .zshrc) — env var written automatically
54
+ ⚠ Continue — detected, manual config required
55
+ ⚠ Cursor — detected, manual config required
56
+ ⚠ Aider — detected, manual config required
57
+
58
+ DOCS
59
+ ${DOCS_URL}
60
+ `);
61
+ process.exit(0);
62
+ }
63
+
64
+ // ─── Utilities ────────────────────────────────────────────────────────────────
65
+
66
+ function atomicWrite(filePath, content) {
67
+ const dir = path.dirname(filePath);
68
+ const tmp = path.join(dir, `.lattice-tmp-${process.pid}-${Date.now()}`);
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ fs.writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o600 });
71
+ fs.renameSync(tmp, filePath);
72
+ }
73
+
74
+ function readJsonSafe(filePath) {
75
+ try {
76
+ const raw = fs.readFileSync(filePath, 'utf8');
77
+ return JSON.parse(raw);
78
+ } catch (e) {
79
+ if (e.code === 'ENOENT') return null;
80
+ if (e instanceof SyntaxError) {
81
+ console.error(` ⚠ Could not parse ${filePath}: ${e.message}`);
82
+ return null;
83
+ }
84
+ throw e;
85
+ }
86
+ }
87
+
88
+ function fileExists(p) {
89
+ try { fs.accessSync(p); return true; } catch { return false; }
90
+ }
91
+
92
+ function ask(rl, question) {
93
+ return new Promise(resolve => rl.question(question, resolve));
94
+ }
95
+
96
+ function spin(label) {
97
+ const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
98
+ let i = 0;
99
+ const id = setInterval(() => {
100
+ process.stdout.write(`\r${frames[i++ % frames.length]} ${label}`);
101
+ }, 80);
102
+ return {
103
+ stop(result) {
104
+ clearInterval(id);
105
+ process.stdout.write(`\r${result}\n`);
106
+ }
107
+ };
108
+ }
109
+
110
+ function httpsGet(url, timeoutMs = 8000) {
111
+ return new Promise((resolve, reject) => {
112
+ const req = https.get(url, { timeout: timeoutMs }, res => {
113
+ let body = '';
114
+ res.on('data', d => body += d);
115
+ res.on('end', () => resolve({ status: res.statusCode, body }));
116
+ });
117
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
118
+ req.on('error', reject);
119
+ });
120
+ }
121
+
122
+ function httpsPost(url, payload, timeoutMs = 6000) {
123
+ return new Promise((resolve, reject) => {
124
+ const data = JSON.stringify(payload);
125
+ const u = new URL(url);
126
+ const options = {
127
+ hostname: u.hostname,
128
+ port: u.port || 443,
129
+ path: u.pathname,
130
+ method: 'POST',
131
+ headers: {
132
+ 'Content-Type': 'application/json',
133
+ 'Content-Length': Buffer.byteLength(data),
134
+ 'User-Agent': 'lattice-install/0.1.0',
135
+ },
136
+ timeout: timeoutMs,
137
+ };
138
+ const req = https.request(options, res => {
139
+ let body = '';
140
+ res.on('data', d => body += d);
141
+ res.on('end', () => resolve({ status: res.statusCode, body }));
142
+ });
143
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
144
+ req.on('error', reject);
145
+ req.write(data);
146
+ req.end();
147
+ });
148
+ }
149
+
150
+ // ─── Detection ────────────────────────────────────────────────────────────────
151
+
152
+ function detectTools() {
153
+ const found = {
154
+ openclaw: false,
155
+ continue: false,
156
+ aider: false,
157
+ cursor: false,
158
+ hasEnvFile: false,
159
+ hasShellProfile: [],
160
+ existingKeys: [],
161
+ };
162
+
163
+ found.openclaw = fileExists(PATHS.openclaw);
164
+ found.continue = fileExists(PATHS.continue);
165
+ found.aider = fileExists(PATHS.aider);
166
+ found.cursor = fileExists(PATHS.cursor);
167
+ found.hasEnvFile = fileExists(PATHS.envFile);
168
+
169
+ if (fileExists(PATHS.bashrc)) found.hasShellProfile.push('.bashrc');
170
+ if (fileExists(PATHS.zshrc)) found.hasShellProfile.push('.zshrc');
171
+
172
+ // Sniff for existing keys in env/shell files
173
+ const sniffPaths = [PATHS.envFile, PATHS.bashrc, PATHS.zshrc];
174
+ for (const p of sniffPaths) {
175
+ if (!fileExists(p)) continue;
176
+ try {
177
+ const content = fs.readFileSync(p, 'utf8');
178
+ if (/ANTHROPIC_API_KEY\s*=\s*sk-ant-/.test(content)) {
179
+ const m = content.match(/ANTHROPIC_API_KEY\s*=\s*(sk-ant-[^\s"']+)/);
180
+ if (m) found.existingKeys.push({ provider: 'anthropic', key: m[1], source: p });
181
+ }
182
+ if (/OPENAI_API_KEY\s*=\s*sk-/.test(content)) {
183
+ const m = content.match(/OPENAI_API_KEY\s*=\s*(sk-[^\s"']+)/);
184
+ if (m) found.existingKeys.push({ provider: 'openai', key: m[1], source: p });
185
+ }
186
+ } catch { /* ignore read errors */ }
187
+ }
188
+
189
+ return found;
190
+ }
191
+
192
+ // ─── Configure OpenClaw ───────────────────────────────────────────────────────
193
+
194
+ function configureOpenclaw(apiKey) {
195
+ let config = readJsonSafe(PATHS.openclaw) || {};
196
+
197
+ // Ensure nested structure exists
198
+ config.models = config.models || {};
199
+ config.models.providers = config.models.providers || {};
200
+ config.agents = config.agents || {};
201
+ config.agents.defaults = config.agents.defaults || {};
202
+ config.agents.defaults.model = config.agents.defaults.model || {};
203
+
204
+ // Set lattice provider
205
+ config.models.providers.lattice = {
206
+ baseUrl: 'https://latticeproxy.io/v1',
207
+ apiKey: apiKey,
208
+ api: 'openai-completions',
209
+ models: [
210
+ { id: 'lattice-sonnet', name: 'Claude Sonnet (via Lattice)', api: 'openai-completions', contextWindow: 200000, maxTokens: 16000 },
211
+ { id: 'lattice-opus', name: 'Claude Opus (via Lattice)', api: 'openai-completions', contextWindow: 200000, maxTokens: 32000 },
212
+ { id: 'lattice-haiku', name: 'Claude Haiku (via Lattice)', api: 'openai-completions', contextWindow: 200000, maxTokens: 8000 },
213
+ ],
214
+ };
215
+
216
+ // Set default model
217
+ config.agents.defaults.model.primary = 'lattice/lattice-sonnet';
218
+
219
+ atomicWrite(PATHS.openclaw, JSON.stringify(config, null, 2) + '\n');
220
+ }
221
+
222
+ // ─── Configure shell profile ──────────────────────────────────────────────────
223
+
224
+ function appendShellExport(profilePath, lines) {
225
+ const marker = '# Added by lattice-install';
226
+ let existing = '';
227
+ try { existing = fs.readFileSync(profilePath, 'utf8'); } catch { /* new file */ }
228
+
229
+ if (existing.includes(marker)) {
230
+ // Replace the block
231
+ const block = `${marker}\n${lines.join('\n')}\n# End lattice-install\n`;
232
+ const replaced = existing.replace(
233
+ /# Added by lattice-install[\s\S]*?# End lattice-install\n?/,
234
+ block,
235
+ );
236
+ atomicWrite(profilePath, replaced);
237
+ } else {
238
+ const block = `\n${marker}\n${lines.join('\n')}\n# End lattice-install\n`;
239
+ atomicWrite(profilePath, existing + block);
240
+ }
241
+ }
242
+
243
+ function configureShellProfiles(apiKey, provider, proxyUrl, profiles) {
244
+ const lines = provider === 'anthropic'
245
+ ? [
246
+ `export ANTHROPIC_API_KEY="${apiKey}"`,
247
+ `export ANTHROPIC_BASE_URL="${proxyUrl}"`,
248
+ ]
249
+ : [
250
+ `export OPENAI_API_KEY="${apiKey}"`,
251
+ `export OPENAI_BASE_URL="${proxyUrl}"`,
252
+ ];
253
+
254
+ for (const profile of profiles) {
255
+ const fullPath = profile === '.bashrc' ? PATHS.bashrc : PATHS.zshrc;
256
+ try {
257
+ appendShellExport(fullPath, lines);
258
+ } catch (e) {
259
+ if (e.code === 'EACCES') {
260
+ console.error(` ⚠ Permission denied writing to ~/${profile} — skipping`);
261
+ } else {
262
+ throw e;
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ // ─── Test connection ──────────────────────────────────────────────────────────
269
+
270
+ async function testConnection(apiKey, provider, proxyUrl) {
271
+ const spinner = spin('Testing connection to Lattice proxy…');
272
+ try {
273
+ // Hit the proxy health/models endpoint
274
+ const testUrl = `${proxyUrl}/models`;
275
+ const u = new URL(testUrl);
276
+ const options = {
277
+ hostname: u.hostname,
278
+ port: u.port || 443,
279
+ path: u.pathname,
280
+ method: 'GET',
281
+ headers: {
282
+ 'Authorization': `Bearer ${apiKey}`,
283
+ 'User-Agent': 'lattice-install/0.1.0',
284
+ },
285
+ timeout: 10000,
286
+ };
287
+ const result = await new Promise((resolve, reject) => {
288
+ const req = https.request(options, res => {
289
+ let body = '';
290
+ res.on('data', d => body += d);
291
+ res.on('end', () => resolve({ status: res.statusCode, body }));
292
+ });
293
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
294
+ req.on('error', reject);
295
+ req.end();
296
+ });
297
+ // Accept 200 or 401 (proxy reachable, auth may vary)
298
+ if (result.status < 500) {
299
+ spinner.stop('✓ Connection successful');
300
+ return true;
301
+ } else {
302
+ spinner.stop(`⚠ Proxy returned HTTP ${result.status} — check your key`);
303
+ return false;
304
+ }
305
+ } catch (e) {
306
+ spinner.stop(`⚠ Could not reach ${proxyUrl} (${e.message}) — check network`);
307
+ return false;
308
+ }
309
+ }
310
+
311
+ // ─── Report telemetry ─────────────────────────────────────────────────────────
312
+
313
+ function fireAndForgetReport(payload) {
314
+ httpsPost(REPORT_URL, payload, 6000).catch(() => { /* silently ignore */ });
315
+ }
316
+
317
+ // ─── Main ─────────────────────────────────────────────────────────────────────
318
+
319
+ async function main() {
320
+ // Step 1: Banner
321
+ console.log(`
322
+ ╔══════════════════════════════════════════════════════╗
323
+ ║ Lattice Network — Install & Onboard ║
324
+ ║ Route your AI traffic through BFT consensus ║
325
+ ╚══════════════════════════════════════════════════════╝
326
+ `);
327
+
328
+ // Step 2: Detect tools
329
+ console.log('Scanning for AI tools…\n');
330
+ const detected = detectTools();
331
+
332
+ const detectedList = [];
333
+ if (detected.openclaw) detectedList.push('OpenClaw');
334
+ if (detected.continue) detectedList.push('Continue');
335
+ if (detected.cursor) detectedList.push('Cursor');
336
+ if (detected.aider) detectedList.push('Aider');
337
+ if (detected.hasShellProfile.length) detectedList.push(`shell (${detected.hasShellProfile.join(', ')})`);
338
+
339
+ if (detectedList.length) {
340
+ console.log(' Detected: ' + detectedList.join(', '));
341
+ } else {
342
+ console.log(' No known AI tools detected — will configure shell profile');
343
+ }
344
+
345
+ // Warn about Option-A tools
346
+ const optionATools = [];
347
+ if (detected.continue) {
348
+ console.log(`\n ⚠ Continue detected — manual config needed, see ${DOCS_URL}`);
349
+ optionATools.push('continue');
350
+ }
351
+ if (detected.cursor) {
352
+ console.log(`\n ⚠ Cursor detected — manual config needed, see ${DOCS_URL}`);
353
+ optionATools.push('cursor');
354
+ }
355
+ if (detected.aider) {
356
+ console.log(`\n ⚠ Aider detected — manual config needed, see ${DOCS_URL}`);
357
+ optionATools.push('aider');
358
+ }
359
+
360
+ console.log('');
361
+
362
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
363
+
364
+ try {
365
+ // Step 3: Ask provider
366
+ const providerAnswer = await ask(rl,
367
+ 'Which AI provider? [1] Anthropic (default) [2] OpenAI\n> '
368
+ );
369
+ const provider = providerAnswer.trim() === '2' ? 'openai' : 'anthropic';
370
+ const keyPrefix = provider === 'anthropic' ? 'sk-ant-' : 'sk-';
371
+ const keyHint = provider === 'anthropic' ? 'sk-ant-…' : 'sk-…';
372
+ const envVar = provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY';
373
+ const baseEnvVar = provider === 'anthropic' ? 'ANTHROPIC_BASE_URL' : 'OPENAI_BASE_URL';
374
+ const proxyUrl = LATTICE_PROXY_URL;
375
+
376
+ console.log(`\n Provider: ${provider === 'anthropic' ? 'Anthropic' : 'OpenAI'}`);
377
+
378
+ // Step 4: Ask for API key
379
+ let apiKey = '';
380
+ while (true) {
381
+ apiKey = (await ask(rl, `\nEnter your ${envVar} (${keyHint}):\n> `)).trim();
382
+
383
+ if (!apiKey) {
384
+ console.log(' API key cannot be empty.');
385
+ continue;
386
+ }
387
+ // Validate format
388
+ if (provider === 'anthropic' && !apiKey.startsWith('sk-ant-')) {
389
+ console.log(' Invalid key format. Anthropic keys start with sk-ant-');
390
+ continue;
391
+ }
392
+ if (provider === 'openai' && !apiKey.startsWith('sk-')) {
393
+ console.log(' Invalid key format. OpenAI keys start with sk-');
394
+ continue;
395
+ }
396
+ break;
397
+ }
398
+
399
+ rl.close();
400
+
401
+ // Step 5: Write config
402
+ console.log('\nConfiguring…\n');
403
+
404
+ if (detected.openclaw) {
405
+ try {
406
+ configureOpenclaw(apiKey);
407
+ console.log(' ✓ OpenClaw configured');
408
+ } catch (e) {
409
+ if (e.code === 'EACCES') {
410
+ console.error(` ⚠ Permission denied writing OpenClaw config — skipping`);
411
+ } else {
412
+ throw e;
413
+ }
414
+ }
415
+ }
416
+
417
+ // Always write shell profile if any exist; if none found, create .bashrc
418
+ const profiles = detected.hasShellProfile.length
419
+ ? detected.hasShellProfile
420
+ : ['.bashrc'];
421
+
422
+ try {
423
+ configureShellProfiles(apiKey, provider, proxyUrl, profiles);
424
+ console.log(` ✓ Shell profile updated (${profiles.join(', ')})`);
425
+ console.log(` Added: ${envVar}, ${baseEnvVar}`);
426
+ } catch (e) {
427
+ if (e.code === 'EACCES') {
428
+ console.error(' ⚠ Permission denied writing shell profile');
429
+ } else {
430
+ throw e;
431
+ }
432
+ }
433
+
434
+ // Step 6: Test connection
435
+ console.log('');
436
+ const connected = await testConnection(apiKey, provider, proxyUrl);
437
+
438
+ // Step 7: Fire-and-forget telemetry report
439
+ const reportPayload = {
440
+ version: '0.1.0',
441
+ provider,
442
+ tools: {
443
+ openclaw: detected.openclaw,
444
+ continue: detected.continue,
445
+ cursor: detected.cursor,
446
+ aider: detected.aider,
447
+ shellProfiles: detected.hasShellProfile,
448
+ unknown: optionATools,
449
+ },
450
+ connected,
451
+ platform: process.platform,
452
+ nodeVersion: process.version,
453
+ timestamp: new Date().toISOString(),
454
+ };
455
+ fireAndForgetReport(reportPayload);
456
+
457
+ // Step 8: Success message
458
+ console.log(`
459
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
460
+
461
+ ✓ Lattice Network configured!
462
+
463
+ Your AI requests now route through Lattice's BFT
464
+ consensus network for verifiable, auditable inference.
465
+
466
+ Reload your shell to apply profile changes:
467
+ source ~/${profiles[0]}
468
+
469
+ Docs & dashboard: ${LATTICE_BASE_URL}
470
+
471
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
472
+ `);
473
+
474
+ process.exit(0);
475
+
476
+ } catch (e) {
477
+ rl.close();
478
+ if (e.code === 'ERR_USE_AFTER_CLOSE' || e.message === 'readline was closed') {
479
+ // User hit Ctrl+C
480
+ console.log('\n\nAborted.');
481
+ process.exit(1);
482
+ }
483
+ console.error(`\nFatal error: ${e.message}`);
484
+ process.exit(1);
485
+ }
486
+ }
487
+
488
+ main().catch(e => {
489
+ console.error(`\nFatal error: ${e.message}`);
490
+ process.exit(1);
491
+ });
package/package.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "lattice-install",
3
+ "version": "0.1.0",
4
+ "description": "Onboard to Lattice Network in 60 seconds",
5
+ "bin": {"lattice-install": "bin/lattice-install.js"},
6
+ "engines": {"node": ">=18"},
7
+ "keywords": ["lattice", "ai", "proxy", "anthropic", "openai", "bft"],
8
+ "license": "MIT"
9
+ }